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 {
|
||||
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";
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
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/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(),
|
||||
|
||||
@ -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(
|
||||
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: const [
|
||||
// Add your tab content here later
|
||||
children: [
|
||||
// Profile Tab
|
||||
_buildProfileTab(),
|
||||
|
||||
// Jobs Tab - empty
|
||||
Container(color: Colors.white),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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,196 +19,157 @@ 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),
|
||||
/// Header Row: Avatar | Name & Tags | Status
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(project["name"],
|
||||
fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
MyText.bodySmall(project["description"],
|
||||
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),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
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: [
|
||||
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),
|
||||
_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]),
|
||||
],
|
||||
),
|
||||
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(
|
||||
color: project["status"] == "Completed"
|
||||
? Colors.green.shade100
|
||||
: project["status"] == "In Progress"
|
||||
? Colors.orange.shade100
|
||||
: Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.indigo.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: MyText.labelSmall(
|
||||
project["status"],
|
||||
fontWeight: 600,
|
||||
color: project["status"] == "Completed"
|
||||
? Colors.green
|
||||
: project["status"] == "In Progress"
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
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,
|
||||
),
|
||||
),
|
||||
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(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
|
||||
/// --- SAME APPBAR AS DETAILS SCREEN ---
|
||||
/// APPBAR
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
@ -249,45 +211,14 @@ 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],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -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(
|
||||
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: filteredProjects.isEmpty
|
||||
child: projects.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.only(
|
||||
left: 8, right: 8, top: 4, bottom: 80),
|
||||
itemCount: filteredProjects.length,
|
||||
itemCount: projects.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildProjectCard(filteredProjects[index]),
|
||||
_buildProjectCard(projects[index]),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user