added service projet screen
This commit is contained in:
parent
8fb65d31e2
commit
cb00911983
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
class ApiEndpoints {
|
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://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.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 getMasterCurrencies = "/Master/currencies/list";
|
||||||
static const String getMasterExpensesCategories =
|
static const String getMasterExpensesCategories =
|
||||||
@ -127,4 +129,8 @@ class ApiEndpoints {
|
|||||||
|
|
||||||
static const String getAssignedServices = "/Project/get/assigned/services";
|
static const String getAssignedServices = "/Project/get/assigned/services";
|
||||||
static const String getAdvancePayments = '/Expense/get/transactions';
|
static const String getAdvancePayments = '/Expense/get/transactions';
|
||||||
|
|
||||||
|
// Service Project Module API Endpoints
|
||||||
|
static const String getServiceProjectsList = "/serviceproject/list";
|
||||||
|
static const String getServiceProjectDetail = "/serviceproject/details";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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_filter.dart';
|
||||||
import 'package:marco/model/finance/payment_request_details_model.dart';
|
import 'package:marco/model/finance/payment_request_details_model.dart';
|
||||||
import 'package:marco/model/finance/advance_payment_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 {
|
class ApiService {
|
||||||
static const bool enableLogs = true;
|
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
|
/// Edit Expense Payment Request
|
||||||
static Future<bool> editExpensePaymentRequestApi({
|
static Future<bool> editExpensePaymentRequestApi({
|
||||||
required String id,
|
required String id,
|
||||||
|
|||||||
241
lib/model/service_project/service_projects_details_model.dart
Normal file
241
lib/model/service_project/service_projects_details_model.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
127
lib/model/service_project/service_projects_list_model.dart
Normal file
127
lib/model/service_project/service_projects_list_model.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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/finance_screen.dart';
|
||||||
import 'package:marco/view/finance/advance_payment_screen.dart';
|
import 'package:marco/view/finance/advance_payment_screen.dart';
|
||||||
import 'package:marco/view/finance/payment_request_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';
|
import 'package:marco/view/service_project/service_project_screen.dart';
|
||||||
class AuthMiddleware extends GetMiddleware {
|
class AuthMiddleware extends GetMiddleware {
|
||||||
@override
|
@override
|
||||||
@ -136,11 +135,6 @@ getPageRoute() {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Service Projects
|
// Service Projects
|
||||||
GetPage(
|
|
||||||
name: '/dashboard/service-project-details',
|
|
||||||
page: () => ServiceProjectDetailsScreen(),
|
|
||||||
middlewares: [AuthMiddleware()],
|
|
||||||
),
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/dashboard/service-projects',
|
name: '/dashboard/service-projects',
|
||||||
page: () => ServiceProjectScreen(),
|
page: () => ServiceProjectScreen(),
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:marco/controller/project_controller.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_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
|
||||||
class ServiceProjectDetailsScreen extends StatefulWidget {
|
class ServiceProjectDetailsScreen extends StatefulWidget {
|
||||||
const ServiceProjectDetailsScreen({super.key});
|
final String projectId;
|
||||||
|
|
||||||
|
const ServiceProjectDetailsScreen({super.key, required this.projectId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ServiceProjectDetailsScreen> createState() =>
|
State<ServiceProjectDetailsScreen> createState() =>
|
||||||
@ -13,13 +18,17 @@ class ServiceProjectDetailsScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServiceProjectDetailsScreenState
|
class _ServiceProjectDetailsScreenState
|
||||||
extends State<ServiceProjectDetailsScreen> with SingleTickerProviderStateMixin {
|
extends State<ServiceProjectDetailsScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
|
final ServiceProjectDetailsController controller =
|
||||||
|
Get.put(ServiceProjectDetailsController());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
|
controller.setProjectId(widget.projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,6 +37,263 @@ class _ServiceProjectDetailsScreenState
|
|||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -47,7 +313,7 @@ class _ServiceProjectDetailsScreenState
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
icon: const Icon(Icons.arrow_back_ios_new,
|
||||||
color: Colors.black, size: 20),
|
color: Colors.black, size: 20),
|
||||||
onPressed: () => Get.offNamed('/dashboard'),
|
onPressed: () => Get.toNamed('/dashboard/service-projects'),
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -101,6 +367,8 @@ class _ServiceProjectDetailsScreenState
|
|||||||
labelColor: Colors.black,
|
labelColor: Colors.black,
|
||||||
unselectedLabelColor: Colors.grey,
|
unselectedLabelColor: Colors.grey,
|
||||||
indicatorColor: Colors.red,
|
indicatorColor: Colors.red,
|
||||||
|
indicatorWeight: 3,
|
||||||
|
isScrollable: false,
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(text: "Profile"),
|
Tab(text: "Profile"),
|
||||||
Tab(text: "Jobs"),
|
Tab(text: "Jobs"),
|
||||||
@ -110,12 +378,25 @@ class _ServiceProjectDetailsScreenState
|
|||||||
|
|
||||||
// ---------------- TabBarView ----------------
|
// ---------------- TabBarView ----------------
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
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,
|
controller: _tabController,
|
||||||
children: const [
|
children: [
|
||||||
// Add your tab content here later
|
// Profile Tab
|
||||||
|
_buildProfileTab(),
|
||||||
|
|
||||||
|
// Jobs Tab - empty
|
||||||
|
Container(color: Colors.white),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
import 'package:marco/controller/service_project/service_project_screen_controller.dart';
|
||||||
import 'package:marco/controller/project_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 {
|
class ServiceProjectScreen extends StatefulWidget {
|
||||||
const ServiceProjectScreen({super.key});
|
const ServiceProjectScreen({super.key});
|
||||||
@ -18,196 +19,157 @@ class ServiceProjectScreen extends StatefulWidget {
|
|||||||
class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||||
with UIMixin {
|
with UIMixin {
|
||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
final RxList<Map<String, dynamic>> allProjects = <Map<String, dynamic>>[].obs;
|
final ServiceProjectController controller =
|
||||||
final RxList<Map<String, dynamic>> filteredProjects =
|
Get.put(ServiceProjectController());
|
||||||
<Map<String, dynamic>>[].obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadProjects();
|
controller.fetchProjects();
|
||||||
}
|
searchController.addListener(() {
|
||||||
|
controller.updateSearch(searchController.text);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshProjects() async {
|
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(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
elevation: 3,
|
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||||
shadowColor: Colors.grey.withOpacity(0.3),
|
shadowColor: Colors.indigo.withOpacity(0.10),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(14),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// TODO: Navigate to Project Details screen
|
// Navigate to ServiceProjectDetailsScreen
|
||||||
|
Get.to(
|
||||||
|
() => ServiceProjectDetailsScreen(projectId: project.id),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||||
child: Row(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Avatar(
|
/// Header Row: Avatar | Name & Tags | Status
|
||||||
firstName: project["name"].split(" ").first,
|
Row(
|
||||||
lastName: project["name"].split(" ").length > 1
|
children: [
|
||||||
? project["name"].split(" ").last
|
|
||||||
: "",
|
|
||||||
size: 40,
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.titleSmall(project["name"],
|
MyText.titleMedium(
|
||||||
fontWeight: 600, overflow: TextOverflow.ellipsis),
|
project.name,
|
||||||
MyText.bodySmall(project["description"],
|
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],
|
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,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(4),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
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(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.phone_outlined,
|
_buildStatColumn(Icons.people_alt_rounded, "Team",
|
||||||
size: 16, color: Colors.indigo),
|
"${project.teamSize}", Colors.blue[700]),
|
||||||
MySpacing.width(4),
|
_buildStatColumn(
|
||||||
Expanded(
|
Icons.check_circle,
|
||||||
child: MyText.labelSmall(
|
"Completed",
|
||||||
project["phone"],
|
"${project.completedWork.toStringAsFixed(1)}%",
|
||||||
color: Colors.indigo,
|
Colors.green[600]),
|
||||||
decoration: TextDecoration.underline,
|
_buildStatColumn(
|
||||||
overflow: TextOverflow.ellipsis,
|
Icons.pending,
|
||||||
),
|
"Planned",
|
||||||
),
|
"${project.plannedWork.toStringAsFixed(1)}%",
|
||||||
MySpacing.width(8),
|
Colors.orange[800]),
|
||||||
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(
|
),
|
||||||
children: [
|
);
|
||||||
Container(
|
}
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
// Helper to build colored tags
|
||||||
|
Widget _buildTag(String label) {
|
||||||
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: project["status"] == "Completed"
|
color: Colors.indigo.withOpacity(0.08),
|
||||||
? Colors.green.shade100
|
borderRadius: BorderRadius.circular(6),
|
||||||
: project["status"] == "In Progress"
|
|
||||||
? Colors.orange.shade100
|
|
||||||
: Colors.red.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
),
|
||||||
child: MyText.labelSmall(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
project["status"],
|
child:
|
||||||
fontWeight: 600,
|
MyText.labelSmall(label, color: Colors.indigo[700], fontWeight: 500),
|
||||||
color: project["status"] == "Completed"
|
);
|
||||||
? Colors.green
|
}
|
||||||
: project["status"] == "In Progress"
|
|
||||||
? Colors.orange
|
// Helper for detail row with icon and text
|
||||||
: Colors.red,
|
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
const Icon(Icons.arrow_forward_ios,
|
|
||||||
color: Colors.grey, size: 20),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
),
|
|
||||||
|
// 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),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +195,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
|
||||||
/// --- SAME APPBAR AS DETAILS SCREEN ---
|
/// APPBAR
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(72),
|
preferredSize: const Size.fromHeight(72),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
@ -249,45 +211,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
icon: const Icon(Icons.arrow_back_ios_new,
|
||||||
color: Colors.black, size: 20),
|
color: Colors.black, size: 20),
|
||||||
onPressed: () => Get.offNamed('/dashboard'),
|
onPressed: () => Get.back(),
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
MyText.titleLarge(
|
MyText.titleLarge(
|
||||||
'Service Projects',
|
'Service Projects',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: Colors.black,
|
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],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -296,7 +227,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
/// --- SEARCH + FILTER BAR ---
|
/// SEARCH + FILTER BAR
|
||||||
Padding(
|
Padding(
|
||||||
padding: MySpacing.xy(8, 8),
|
padding: MySpacing.xy(8, 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -306,7 +237,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
height: 35,
|
height: 35,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: searchController,
|
controller: searchController,
|
||||||
onChanged: _filterProjects,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 12),
|
const EdgeInsets.symmetric(horizontal: 12),
|
||||||
@ -315,15 +245,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
suffixIcon: ValueListenableBuilder<TextEditingValue>(
|
suffixIcon: ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: searchController,
|
valueListenable: searchController,
|
||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
if (value.text.isEmpty) {
|
if (value.text.isEmpty)
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.clear,
|
icon: const Icon(Icons.clear,
|
||||||
size: 20, color: Colors.grey),
|
size: 20, color: Colors.grey),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
_filterProjects('');
|
controller.updateSearch('');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -402,24 +331,31 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// --- PROJECT LIST ---
|
/// PROJECT LIST
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() => MyRefreshIndicator(
|
child: Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final projects = controller.filteredProjects;
|
||||||
|
return MyRefreshIndicator(
|
||||||
onRefresh: _refreshProjects,
|
onRefresh: _refreshProjects,
|
||||||
backgroundColor: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
child: filteredProjects.isEmpty
|
child: projects.isEmpty
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: MySpacing.only(
|
padding: MySpacing.only(
|
||||||
left: 8, right: 8, top: 4, bottom: 80),
|
left: 8, right: 8, top: 4, bottom: 80),
|
||||||
itemCount: filteredProjects.length,
|
itemCount: projects.length,
|
||||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||||
itemBuilder: (_, index) =>
|
itemBuilder: (_, index) =>
|
||||||
_buildProjectCard(filteredProjects[index]),
|
_buildProjectCard(projects[index]),
|
||||||
),
|
),
|
||||||
)),
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -227,8 +227,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
|||||||
final buildings = dailyTasks
|
final buildings = dailyTasks
|
||||||
.expand((task) => task.buildings)
|
.expand((task) => task.buildings)
|
||||||
.where((building) =>
|
.where((building) =>
|
||||||
(building.plannedWork ?? 0) > 0 ||
|
(building.plannedWork ) > 0 ||
|
||||||
(building.completedWork ?? 0) > 0)
|
(building.completedWork ) > 0)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (buildings.isEmpty) {
|
if (buildings.isEmpty) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user