made chnages in details screen and list screen
This commit is contained in:
parent
1de5e7fae7
commit
d229facfba
@ -3,33 +3,56 @@ import 'package:marco/helpers/services/api_service.dart';
|
|||||||
import 'package:marco/model/service_project/service_projects_list_model.dart';
|
import 'package:marco/model/service_project/service_projects_list_model.dart';
|
||||||
|
|
||||||
class ServiceProjectController extends GetxController {
|
class ServiceProjectController extends GetxController {
|
||||||
var projects = <ProjectItem>[].obs;
|
final projects = <ProjectItem>[].obs;
|
||||||
var isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
var searchQuery = ''.obs;
|
final searchQuery = ''.obs;
|
||||||
|
|
||||||
RxList<ProjectItem> get filteredProjects {
|
/// Computed filtered project list
|
||||||
if (searchQuery.value.isEmpty) return projects;
|
List<ProjectItem> get filteredProjects {
|
||||||
return projects
|
final query = searchQuery.value.trim().toLowerCase();
|
||||||
.where((p) =>
|
if (query.isEmpty) return projects;
|
||||||
p.name.toLowerCase().contains(searchQuery.value.toLowerCase()) ||
|
|
||||||
p.contactPerson.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
return projects.where((p) {
|
||||||
.toList()
|
final nameMatch = p.name.toLowerCase().contains(query);
|
||||||
.obs;
|
final shortNameMatch = p.shortName.toLowerCase().contains(query);
|
||||||
|
final addressMatch = p.address.toLowerCase().contains(query);
|
||||||
|
final contactMatch = p.contactName.toLowerCase().contains(query);
|
||||||
|
final clientMatch = p.client != null &&
|
||||||
|
(p.client!.name.toLowerCase().contains(query) ||
|
||||||
|
p.client!.contactPerson.toLowerCase().contains(query));
|
||||||
|
|
||||||
|
return nameMatch ||
|
||||||
|
shortNameMatch ||
|
||||||
|
addressMatch ||
|
||||||
|
contactMatch ||
|
||||||
|
clientMatch;
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch projects from API
|
||||||
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
|
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final result = await ApiService.getServiceProjectsListApi(
|
final result = await ApiService.getServiceProjectsListApi(
|
||||||
pageNumber: pageNumber, pageSize: pageSize);
|
pageNumber: pageNumber,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
if (result != null && result.data != null) {
|
if (result != null && result.data != null) {
|
||||||
projects.assignAll(result.data!.data ?? []);
|
projects.assignAll(result.data!.data);
|
||||||
|
} else {
|
||||||
|
projects.clear();
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Optional: log or show error
|
||||||
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update search
|
||||||
void updateSearch(String query) {
|
void updateSearch(String query) {
|
||||||
searchQuery.value = query;
|
searchQuery.value = query;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class ServiceProjectListModel {
|
|||||||
data: json['data'] != null ? ProjectData.fromJson(json['data']) : null,
|
data: json['data'] != null ? ProjectData.fromJson(json['data']) : null,
|
||||||
errors: json['errors'],
|
errors: json['errors'],
|
||||||
statusCode: json['statusCode'] ?? 0,
|
statusCode: json['statusCode'] ?? 0,
|
||||||
timestamp: DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()),
|
timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,13 +40,13 @@ class ProjectData {
|
|||||||
final int currentPage;
|
final int currentPage;
|
||||||
final int totalPages;
|
final int totalPages;
|
||||||
final int totalEntities;
|
final int totalEntities;
|
||||||
final List<ProjectItem>? data;
|
final List<ProjectItem> data;
|
||||||
|
|
||||||
ProjectData({
|
ProjectData({
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
required this.totalPages,
|
required this.totalPages,
|
||||||
required this.totalEntities,
|
required this.totalEntities,
|
||||||
this.data,
|
required this.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProjectData.fromJson(Map<String, dynamic> json) {
|
factory ProjectData.fromJson(Map<String, dynamic> json) {
|
||||||
@ -55,7 +55,8 @@ class ProjectData {
|
|||||||
totalPages: json['totalPages'] ?? 1,
|
totalPages: json['totalPages'] ?? 1,
|
||||||
totalEntities: json['totalEntites'] ?? 0,
|
totalEntities: json['totalEntites'] ?? 0,
|
||||||
data: json['data'] != null
|
data: json['data'] != null
|
||||||
? List<ProjectItem>.from(json['data'].map((x) => ProjectItem.fromJson(x)))
|
? List<ProjectItem>.from(
|
||||||
|
json['data'].map((x) => ProjectItem.fromJson(x)))
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -64,7 +65,7 @@ class ProjectData {
|
|||||||
'currentPage': currentPage,
|
'currentPage': currentPage,
|
||||||
'totalPages': totalPages,
|
'totalPages': totalPages,
|
||||||
'totalEntites': totalEntities,
|
'totalEntites': totalEntities,
|
||||||
'data': data?.map((x) => x.toJson()).toList(),
|
'data': data.map((x) => x.toJson()).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,27 +73,31 @@ class ProjectItem {
|
|||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String shortName;
|
final String shortName;
|
||||||
final String projectAddress;
|
final String address;
|
||||||
final String contactPerson;
|
final DateTime assignedDate;
|
||||||
final DateTime startDate;
|
final Status? status;
|
||||||
final DateTime endDate;
|
final Client? client;
|
||||||
final String projectStatusId;
|
final List<Service> services;
|
||||||
final int teamSize;
|
final String contactName;
|
||||||
final double completedWork;
|
final String contactPhone;
|
||||||
final double plannedWork;
|
final String contactEmail;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final CreatedBy? createdBy;
|
||||||
|
|
||||||
ProjectItem({
|
ProjectItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.shortName,
|
required this.shortName,
|
||||||
required this.projectAddress,
|
required this.address,
|
||||||
required this.contactPerson,
|
required this.assignedDate,
|
||||||
required this.startDate,
|
this.status,
|
||||||
required this.endDate,
|
this.client,
|
||||||
required this.projectStatusId,
|
required this.services,
|
||||||
required this.teamSize,
|
required this.contactName,
|
||||||
required this.completedWork,
|
required this.contactPhone,
|
||||||
required this.plannedWork,
|
required this.contactEmail,
|
||||||
|
required this.createdAt,
|
||||||
|
this.createdBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProjectItem.fromJson(Map<String, dynamic> json) {
|
factory ProjectItem.fromJson(Map<String, dynamic> json) {
|
||||||
@ -100,14 +105,24 @@ class ProjectItem {
|
|||||||
id: json['id'] ?? '',
|
id: json['id'] ?? '',
|
||||||
name: json['name'] ?? '',
|
name: json['name'] ?? '',
|
||||||
shortName: json['shortName'] ?? '',
|
shortName: json['shortName'] ?? '',
|
||||||
projectAddress: json['projectAddress'] ?? '',
|
address: json['address'] ?? '',
|
||||||
contactPerson: json['contactPerson'] ?? '',
|
assignedDate:
|
||||||
startDate: DateTime.parse(json['startDate'] ?? DateTime.now().toIso8601String()),
|
DateTime.tryParse(json['assignedDate'] ?? '') ?? DateTime.now(),
|
||||||
endDate: DateTime.parse(json['endDate'] ?? DateTime.now().toIso8601String()),
|
status:
|
||||||
projectStatusId: json['projectStatusId'] ?? '',
|
json['status'] != null ? Status.fromJson(json['status']) : null,
|
||||||
teamSize: json['teamSize'] ?? 0,
|
client:
|
||||||
completedWork: (json['completedWork']?.toDouble() ?? 0.0),
|
json['client'] != null ? Client.fromJson(json['client']) : null,
|
||||||
plannedWork: (json['plannedWork']?.toDouble() ?? 0.0),
|
services: json['services'] != null
|
||||||
|
? List<Service>.from(json['services'].map((x) => Service.fromJson(x)))
|
||||||
|
: [],
|
||||||
|
contactName: json['contactName'] ?? '',
|
||||||
|
contactPhone: json['contactPhone'] ?? '',
|
||||||
|
contactEmail: json['contactEmail'] ?? '',
|
||||||
|
createdAt:
|
||||||
|
DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||||
|
createdBy: json['createdBy'] != null
|
||||||
|
? CreatedBy.fromJson(json['createdBy'])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,13 +130,155 @@ class ProjectItem {
|
|||||||
'id': id,
|
'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'shortName': shortName,
|
'shortName': shortName,
|
||||||
'projectAddress': projectAddress,
|
'address': address,
|
||||||
'contactPerson': contactPerson,
|
'assignedDate': assignedDate.toIso8601String(),
|
||||||
'startDate': startDate.toIso8601String(),
|
'status': status?.toJson(),
|
||||||
'endDate': endDate.toIso8601String(),
|
'client': client?.toJson(),
|
||||||
'projectStatusId': projectStatusId,
|
'services': services.map((x) => x.toJson()).toList(),
|
||||||
'teamSize': teamSize,
|
'contactName': contactName,
|
||||||
'completedWork': completedWork,
|
'contactPhone': contactPhone,
|
||||||
'plannedWork': plannedWork,
|
'contactEmail': contactEmail,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'createdBy': createdBy?.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Status {
|
||||||
|
final String id;
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
Status({
|
||||||
|
required this.id,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Status.fromJson(Map<String, dynamic> json) {
|
||||||
|
return 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,
|
||||||
|
required this.email,
|
||||||
|
required this.contactPerson,
|
||||||
|
required this.address,
|
||||||
|
required this.contactNumber,
|
||||||
|
required this.sprid,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Client.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Client(
|
||||||
|
id: json['id'] ?? '',
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
email: json['email'] ?? '',
|
||||||
|
contactPerson: json['contactPerson'] ?? '',
|
||||||
|
address: json['address'] ?? '',
|
||||||
|
contactNumber: json['contactNumber'] ?? '',
|
||||||
|
sprid: json['sprid'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
required this.description,
|
||||||
|
required this.isSystem,
|
||||||
|
required this.isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Service.fromJson(Map<String, dynamic> json) {
|
||||||
|
return 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 CreatedBy {
|
||||||
|
final String id;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final String email;
|
||||||
|
final String photo;
|
||||||
|
final String jobRoleId;
|
||||||
|
final String jobRoleName;
|
||||||
|
|
||||||
|
CreatedBy({
|
||||||
|
required this.id,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.email,
|
||||||
|
required this.photo,
|
||||||
|
required this.jobRoleId,
|
||||||
|
required this.jobRoleName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreatedBy.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CreatedBy(
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,14 +80,9 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: contentTheme.primary.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: contentTheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.width(16),
|
MySpacing.width(16),
|
||||||
@ -95,28 +90,22 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
MyText(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: 500,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
MySpacing.height(4),
|
MySpacing.height(4),
|
||||||
Text(
|
MyText(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
color: isActionable && value != 'NA'
|
color: isActionable && value != 'NA'
|
||||||
? contentTheme.primary
|
? Colors.blueAccent
|
||||||
: Colors.black87,
|
: Colors.black87,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: 500,
|
||||||
decoration: isActionable && value != 'NA'
|
decoration: isActionable && value != 'NA'
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: TextDecoration.none,
|
: TextDecoration.none,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -151,17 +140,14 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|||||||
Icon(
|
Icon(
|
||||||
titleIcon,
|
titleIcon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: contentTheme.primary,
|
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Text(
|
MyText(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: 700,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
@ -198,7 +184,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|||||||
|
|
||||||
final employee = controller.selectedEmployeeDetails.value;
|
final employee = controller.selectedEmployeeDetails.value;
|
||||||
if (employee == null) {
|
if (employee == null) {
|
||||||
return const Center(child: Text('No employee details found.'));
|
return Center(child: MyText('No employee details found.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@ -226,7 +212,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|||||||
Avatar(
|
Avatar(
|
||||||
firstName: employee.firstName,
|
firstName: employee.firstName,
|
||||||
lastName: employee.lastName,
|
lastName: employee.lastName,
|
||||||
size: 45,
|
size: 35,
|
||||||
),
|
),
|
||||||
MySpacing.width(16),
|
MySpacing.width(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -444,9 +430,10 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: contentTheme.primary,
|
backgroundColor: contentTheme.primary,
|
||||||
label: const Text(
|
label: MyText(
|
||||||
'Assign to Project',
|
'Assign to Project',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -362,13 +362,13 @@ class PaymentRequestPermissionHelper {
|
|||||||
|
|
||||||
// ------------------ Sub-widgets ------------------
|
// ------------------ Sub-widgets ------------------
|
||||||
|
|
||||||
class _Header extends StatelessWidget with UIMixin {
|
class _Header extends StatefulWidget {
|
||||||
final PaymentRequestData request;
|
final PaymentRequestData request;
|
||||||
final Color Function(String) colorParser;
|
final Color Function(String) colorParser;
|
||||||
final VoidCallback? onEdit;
|
final VoidCallback? onEdit;
|
||||||
final EmployeeInfo? employeeInfo;
|
final EmployeeInfo? employeeInfo;
|
||||||
|
|
||||||
_Header({
|
const _Header({
|
||||||
required this.request,
|
required this.request,
|
||||||
required this.colorParser,
|
required this.colorParser,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
@ -376,12 +376,17 @@ class _Header extends StatelessWidget with UIMixin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<_Header> createState() => _HeaderState();
|
||||||
final statusColor = colorParser(request.expenseStatus.color);
|
}
|
||||||
|
|
||||||
final canEdit = employeeInfo != null &&
|
class _HeaderState extends State<_Header> with UIMixin {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final statusColor = widget.colorParser(widget.request.expenseStatus.color);
|
||||||
|
|
||||||
|
final canEdit = widget.employeeInfo != null &&
|
||||||
PaymentRequestPermissionHelper.canEditPaymentRequest(
|
PaymentRequestPermissionHelper.canEditPaymentRequest(
|
||||||
employeeInfo, request);
|
widget.employeeInfo, widget.request);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -390,13 +395,13 @@ class _Header extends StatelessWidget with UIMixin {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium(
|
MyText.bodyMedium(
|
||||||
'ID: ${request.paymentRequestUID}',
|
'ID: ${widget.request.paymentRequestUID}',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
if (canEdit)
|
if (canEdit)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: onEdit,
|
onPressed: widget.onEdit,
|
||||||
icon: Icon(Icons.edit, color: contentTheme.primary),
|
icon: Icon(Icons.edit, color: contentTheme.primary),
|
||||||
tooltip: "Edit Payment Request",
|
tooltip: "Edit Payment Request",
|
||||||
),
|
),
|
||||||
@ -417,7 +422,7 @@ class _Header extends StatelessWidget with UIMixin {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: MyText.bodySmall(
|
child: MyText.bodySmall(
|
||||||
DateTimeUtils.convertUtcToLocal(
|
DateTimeUtils.convertUtcToLocal(
|
||||||
request.createdAt.toIso8601String(),
|
widget.request.createdAt.toIso8601String(),
|
||||||
format: 'dd MMM yyyy'),
|
format: 'dd MMM yyyy'),
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -438,7 +443,7 @@ class _Header extends StatelessWidget with UIMixin {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 100,
|
width: 100,
|
||||||
child: MyText.labelSmall(
|
child: MyText.labelSmall(
|
||||||
request.expenseStatus.displayName,
|
widget.request.expenseStatus.displayName,
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -629,21 +634,22 @@ class _DetailsTable extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Basic Info
|
// Basic Info
|
||||||
_labelValueRow("Payment Request ID:", request.paymentRequestUID),
|
_labelValueRow("Payment Request ID:", request.paymentRequestUID),
|
||||||
if (request.paidTransactionId != null && request.paidTransactionId!.isNotEmpty)
|
if (request.paidTransactionId != null &&
|
||||||
|
request.paidTransactionId!.isNotEmpty)
|
||||||
_labelValueRow("Transaction ID:", request.paidTransactionId!),
|
_labelValueRow("Transaction ID:", request.paidTransactionId!),
|
||||||
_labelValueRow("Payee:", request.payee),
|
_labelValueRow("Payee:", request.payee),
|
||||||
_labelValueRow("Project:", request.project.name),
|
_labelValueRow("Project:", request.project.name),
|
||||||
_labelValueRow("Expense Category:", request.expenseCategory.name),
|
_labelValueRow("Expense Category:", request.expenseCategory.name),
|
||||||
|
|
||||||
// Amounts
|
// Amounts
|
||||||
_labelValueRow(
|
_labelValueRow("Amount:",
|
||||||
"Amount:", "${request.currency.symbol} ${request.amount.toStringAsFixed(2)}"),
|
"${request.currency.symbol} ${request.amount.toStringAsFixed(2)}"),
|
||||||
if (request.baseAmount != null)
|
if (request.baseAmount != null)
|
||||||
_labelValueRow(
|
_labelValueRow("Base Amount:",
|
||||||
"Base Amount:", "${request.currency.symbol} ${request.baseAmount!.toStringAsFixed(2)}"),
|
"${request.currency.symbol} ${request.baseAmount!.toStringAsFixed(2)}"),
|
||||||
if (request.taxAmount != null)
|
if (request.taxAmount != null)
|
||||||
_labelValueRow(
|
_labelValueRow("Tax Amount:",
|
||||||
"Tax Amount:", "${request.currency.symbol} ${request.taxAmount!.toStringAsFixed(2)}"),
|
"${request.currency.symbol} ${request.taxAmount!.toStringAsFixed(2)}"),
|
||||||
if (request.expenseCategory.noOfPersonsRequired)
|
if (request.expenseCategory.noOfPersonsRequired)
|
||||||
_labelValueRow("Additional Persons Required:", "Yes"),
|
_labelValueRow("Additional Persons Required:", "Yes"),
|
||||||
if (request.expenseCategory.isAttachmentRequried)
|
if (request.expenseCategory.isAttachmentRequried)
|
||||||
@ -652,43 +658,44 @@ class _DetailsTable extends StatelessWidget {
|
|||||||
// Dates
|
// Dates
|
||||||
_labelValueRow(
|
_labelValueRow(
|
||||||
"Due Date:",
|
"Due Date:",
|
||||||
DateTimeUtils.convertUtcToLocal(
|
DateTimeUtils.convertUtcToLocal(request.dueDate.toIso8601String(),
|
||||||
request.dueDate.toIso8601String(),
|
|
||||||
format: 'dd MMM yyyy')),
|
format: 'dd MMM yyyy')),
|
||||||
_labelValueRow(
|
_labelValueRow(
|
||||||
"Created At:",
|
"Created At:",
|
||||||
DateTimeUtils.convertUtcToLocal(
|
DateTimeUtils.convertUtcToLocal(request.createdAt.toIso8601String(),
|
||||||
request.createdAt.toIso8601String(),
|
|
||||||
format: 'dd MMM yyyy')),
|
format: 'dd MMM yyyy')),
|
||||||
_labelValueRow(
|
_labelValueRow(
|
||||||
"Updated At:",
|
"Updated At:",
|
||||||
DateTimeUtils.convertUtcToLocal(
|
DateTimeUtils.convertUtcToLocal(request.updatedAt.toIso8601String(),
|
||||||
request.updatedAt.toIso8601String(),
|
|
||||||
format: 'dd MMM yyyy')),
|
format: 'dd MMM yyyy')),
|
||||||
|
|
||||||
// Payment Info
|
// Payment Info
|
||||||
if (request.paidAt != null)
|
if (request.paidAt != null)
|
||||||
_labelValueRow(
|
_labelValueRow(
|
||||||
"Transaction Date:",
|
"Transaction Date:",
|
||||||
DateTimeUtils.convertUtcToLocal(
|
DateTimeUtils.convertUtcToLocal(request.paidAt!.toIso8601String(),
|
||||||
request.paidAt!.toIso8601String(),
|
|
||||||
format: 'dd MMM yyyy')),
|
format: 'dd MMM yyyy')),
|
||||||
if (request.paidBy != null)
|
if (request.paidBy != null)
|
||||||
_labelValueRow(
|
_labelValueRow("Paid By:",
|
||||||
"Paid By:", "${request.paidBy!.firstName} ${request.paidBy!.lastName}"),
|
"${request.paidBy!.firstName} ${request.paidBy!.lastName}"),
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
_labelValueRow("Advance Payment:", request.isAdvancePayment ? "Yes" : "No"),
|
_labelValueRow(
|
||||||
_labelValueRow("Expense Created:", request.isExpenseCreated ? "Yes" : "No"),
|
"Advance Payment:", request.isAdvancePayment ? "Yes" : "No"),
|
||||||
|
_labelValueRow(
|
||||||
|
"Expense Created:", request.isExpenseCreated ? "Yes" : "No"),
|
||||||
_labelValueRow("Active:", request.isActive ? "Yes" : "No"),
|
_labelValueRow("Active:", request.isActive ? "Yes" : "No"),
|
||||||
|
|
||||||
// Recurring Payment Info
|
// Recurring Payment Info
|
||||||
if (request.recurringPayment != null) ...[
|
if (request.recurringPayment != null) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
MyText.bodySmall("Recurring Payment Info:", fontWeight: 600),
|
MyText.bodySmall("Recurring Payment Info:", fontWeight: 600),
|
||||||
_labelValueRow("Recurring ID:", request.recurringPayment!.recurringPaymentUID),
|
_labelValueRow(
|
||||||
_labelValueRow("Amount:", "${request.currency.symbol} ${request.recurringPayment!.amount.toStringAsFixed(2)}"),
|
"Recurring ID:", request.recurringPayment!.recurringPaymentUID),
|
||||||
_labelValueRow("Variable Amount:", request.recurringPayment!.isVariable ? "Yes" : "No"),
|
_labelValueRow("Amount:",
|
||||||
|
"${request.currency.symbol} ${request.recurringPayment!.amount.toStringAsFixed(2)}"),
|
||||||
|
_labelValueRow("Variable Amount:",
|
||||||
|
request.recurringPayment!.isVariable ? "Yes" : "No"),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Description & Attachments
|
// Description & Attachments
|
||||||
|
|||||||
@ -20,15 +20,20 @@ class ServiceProjectDetailsScreen extends StatefulWidget {
|
|||||||
class _ServiceProjectDetailsScreenState
|
class _ServiceProjectDetailsScreenState
|
||||||
extends State<ServiceProjectDetailsScreen>
|
extends State<ServiceProjectDetailsScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late final TabController _tabController;
|
||||||
final ServiceProjectDetailsController controller =
|
late 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 = Get.put(ServiceProjectDetailsController());
|
||||||
|
|
||||||
|
// Fetch project detail safely after first frame
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
controller.setProjectId(widget.projectId);
|
controller.setProjectId(widget.projectId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -57,39 +62,34 @@ class _ServiceProjectDetailsScreenState
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
child: Icon(
|
||||||
color: Colors.redAccent.withOpacity(0.1),
|
icon,
|
||||||
borderRadius: BorderRadius.circular(5),
|
size: 20,
|
||||||
),
|
),
|
||||||
child: Icon(icon, size: 20, color: Colors.redAccent),
|
|
||||||
),
|
),
|
||||||
MySpacing.width(16),
|
MySpacing.width(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
MyText.bodySmall(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: 500,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
MySpacing.height(4),
|
MySpacing.height(4),
|
||||||
Text(
|
MyText.bodyMedium(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: isActionable && value != 'NA'
|
color: isActionable && value != 'NA'
|
||||||
? Colors.redAccent
|
? Colors.blueAccent
|
||||||
: Colors.black87,
|
: Colors.black87,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: 500,
|
||||||
decoration: isActionable && value != 'NA'
|
decoration: isActionable && value != 'NA'
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: TextDecoration.none,
|
: TextDecoration.none,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -117,14 +117,13 @@ class _ServiceProjectDetailsScreenState
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(titleIcon, size: 20, color: Colors.redAccent),
|
Icon(titleIcon, size: 20),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Text(
|
MyText.bodyLarge(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: 700,
|
||||||
color: Colors.black87),
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -148,7 +147,9 @@ class _ServiceProjectDetailsScreenState
|
|||||||
|
|
||||||
Widget _buildProfileTab() {
|
Widget _buildProfileTab() {
|
||||||
final project = controller.projectDetail.value;
|
final project = controller.projectDetail.value;
|
||||||
if (project == null) return const Center(child: Text("No project data"));
|
if (project == null) {
|
||||||
|
return Center(child: MyText.bodyMedium("No project data"));
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: MySpacing.all(12),
|
padding: MySpacing.all(12),
|
||||||
@ -166,8 +167,7 @@ class _ServiceProjectDetailsScreenState
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.work_outline,
|
const Icon(Icons.work_outline, size: 35),
|
||||||
size: 45, color: Colors.redAccent),
|
|
||||||
MySpacing.width(16),
|
MySpacing.width(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -175,8 +175,8 @@ class _ServiceProjectDetailsScreenState
|
|||||||
children: [
|
children: [
|
||||||
MyText.titleMedium(project.name, fontWeight: 700),
|
MyText.titleMedium(project.name, fontWeight: 700),
|
||||||
MySpacing.height(6),
|
MySpacing.height(6),
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(project.client?.name ?? 'N/A',
|
||||||
project.client?.name ?? 'N/A', fontWeight: 500),
|
fontWeight: 500),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -211,8 +211,7 @@ class _ServiceProjectDetailsScreenState
|
|||||||
label: 'Contact Phone',
|
label: 'Contact Phone',
|
||||||
value: project.contactPhone,
|
value: project.contactPhone,
|
||||||
isActionable: true,
|
isActionable: true,
|
||||||
onTap: () =>
|
onTap: () => LauncherUtils.launchPhone(project.contactPhone),
|
||||||
LauncherUtils.launchPhone(project.contactPhone),
|
|
||||||
onLongPress: () => LauncherUtils.copyToClipboard(
|
onLongPress: () => LauncherUtils.copyToClipboard(
|
||||||
project.contactPhone,
|
project.contactPhone,
|
||||||
typeLabel: 'Phone'),
|
typeLabel: 'Phone'),
|
||||||
@ -222,8 +221,7 @@ class _ServiceProjectDetailsScreenState
|
|||||||
label: 'Contact Email',
|
label: 'Contact Email',
|
||||||
value: project.contactEmail,
|
value: project.contactEmail,
|
||||||
isActionable: true,
|
isActionable: true,
|
||||||
onTap: () =>
|
onTap: () => LauncherUtils.launchEmail(project.contactEmail),
|
||||||
LauncherUtils.launchEmail(project.contactEmail),
|
|
||||||
onLongPress: () => LauncherUtils.copyToClipboard(
|
onLongPress: () => LauncherUtils.copyToClipboard(
|
||||||
project.contactEmail,
|
project.contactEmail,
|
||||||
typeLabel: 'Email'),
|
typeLabel: 'Email'),
|
||||||
@ -369,9 +367,9 @@ class _ServiceProjectDetailsScreenState
|
|||||||
indicatorColor: Colors.red,
|
indicatorColor: Colors.red,
|
||||||
indicatorWeight: 3,
|
indicatorWeight: 3,
|
||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
tabs: const [
|
tabs: [
|
||||||
Tab(text: "Profile"),
|
Tab(child: MyText.bodyMedium("Profile")),
|
||||||
Tab(text: "Jobs"),
|
Tab(child: MyText.bodyMedium("Jobs")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -383,7 +381,8 @@ class _ServiceProjectDetailsScreenState
|
|||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (controller.errorMessage.value.isNotEmpty) {
|
if (controller.errorMessage.value.isNotEmpty) {
|
||||||
return Center(child: Text(controller.errorMessage.value));
|
return Center(
|
||||||
|
child: MyText.bodyMedium(controller.errorMessage.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return TabBarView(
|
return TabBarView(
|
||||||
|
|||||||
@ -21,11 +21,15 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
final ServiceProjectController controller =
|
final ServiceProjectController controller =
|
||||||
Get.put(ServiceProjectController());
|
Get.put(ServiceProjectController());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Fetch projects safely after first frame
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
controller.fetchProjects();
|
controller.fetchProjects();
|
||||||
|
});
|
||||||
|
|
||||||
searchController.addListener(() {
|
searchController.addListener(() {
|
||||||
controller.updateSearch(searchController.text);
|
controller.updateSearch(searchController.text);
|
||||||
});
|
});
|
||||||
@ -45,17 +49,16 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to ServiceProjectDetailsScreen
|
// Navigate to ServiceProjectDetailsScreen
|
||||||
Get.to(
|
Get.to(() => ServiceProjectDetailsScreen(projectId: project.id));
|
||||||
() => ServiceProjectDetailsScreen(projectId: project.id),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
/// Header Row: Avatar | Name & Tags | Status
|
/// Project Header
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -63,61 +66,70 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
children: [
|
children: [
|
||||||
MyText.titleMedium(
|
MyText.titleMedium(
|
||||||
project.name,
|
project.name,
|
||||||
fontWeight: 800,
|
fontWeight: 700,
|
||||||
),
|
|
||||||
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(4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
if (project.status?.status.isNotEmpty ?? false)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.indigo.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 4),
|
||||||
|
child: MyText.labelSmall(
|
||||||
|
project.status!.status,
|
||||||
|
color: Colors.indigo[700],
|
||||||
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
MySpacing.height(12),
|
MySpacing.height(10),
|
||||||
|
|
||||||
|
/// Assigned Date
|
||||||
_buildDetailRow(
|
_buildDetailRow(
|
||||||
Icons.date_range_outlined,
|
Icons.date_range_outlined,
|
||||||
Colors.teal,
|
Colors.teal,
|
||||||
"${DateTimeUtils.convertUtcToLocal(project.startDate.toIso8601String(), format: DateTimeUtils.defaultFormat)} To "
|
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
|
||||||
"${DateTimeUtils.convertUtcToLocal(project.endDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
|
||||||
|
MySpacing.height(8),
|
||||||
|
|
||||||
|
/// Client Info
|
||||||
|
if (project.client != null)
|
||||||
|
_buildDetailRow(
|
||||||
|
Icons.account_circle_outlined,
|
||||||
|
Colors.indigo,
|
||||||
|
"Client: ${project.client!.name} (${project.client!.contactPerson})",
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
|
||||||
|
MySpacing.height(8),
|
||||||
|
|
||||||
|
/// Contact Info
|
||||||
|
_buildDetailRow(
|
||||||
|
Icons.phone,
|
||||||
|
Colors.green,
|
||||||
|
"Contact: ${project.contactName} (${project.contactPhone})",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
|
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
|
|
||||||
/// Stats
|
/// Services List
|
||||||
Row(
|
if (project.services.isNotEmpty)
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Wrap(
|
||||||
children: [
|
spacing: 6,
|
||||||
_buildStatColumn(Icons.people_alt_rounded, "Team",
|
runSpacing: 4,
|
||||||
"${project.teamSize}", Colors.blue[700]),
|
children: project.services
|
||||||
_buildStatColumn(
|
.map((service) => _buildServiceChip(service.name))
|
||||||
Icons.check_circle,
|
.toList(),
|
||||||
"Completed",
|
|
||||||
"${project.completedWork.toStringAsFixed(1)}%",
|
|
||||||
Colors.green[600]),
|
|
||||||
_buildStatColumn(
|
|
||||||
Icons.pending,
|
|
||||||
"Planned",
|
|
||||||
"${project.plannedWork.toStringAsFixed(1)}%",
|
|
||||||
Colors.orange[800]),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -126,25 +138,27 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to build colored tags
|
Widget _buildServiceChip(String name) {
|
||||||
Widget _buildTag(String label) {
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.indigo.withOpacity(0.08),
|
color: Colors.orange.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
child:
|
child: MyText.labelSmall(
|
||||||
MyText.labelSmall(label, color: Colors.indigo[700], fontWeight: 500),
|
name,
|
||||||
|
color: Colors.orange[800],
|
||||||
|
fontWeight: 500,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for detail row with icon and text
|
|
||||||
Widget _buildDetailRow(IconData icon, Color iconColor, String value,
|
Widget _buildDetailRow(IconData icon, Color iconColor, String value,
|
||||||
{double fontSize = 12}) {
|
{double fontSize = 12}) {
|
||||||
return Row(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 19, color: iconColor),
|
Icon(icon, size: 18, color: iconColor),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: MyText.bodySmall(
|
child: MyText.bodySmall(
|
||||||
@ -152,27 +166,12 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
color: Colors.grey[900],
|
color: Colors.grey[900],
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for stats column (icon + label + value)
|
|
||||||
Widget _buildStatColumn(
|
|
||||||
IconData icon, String label, String value, Color? color) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: color, size: 19),
|
|
||||||
SizedBox(height: 3),
|
|
||||||
MyText.labelSmall(value, color: color, fontWeight: 700),
|
|
||||||
MyText.bodySmall(label, color: Colors.grey[500], fontSize: 11),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -194,8 +193,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
|
||||||
/// APPBAR
|
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(72),
|
preferredSize: const Size.fromHeight(72),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
@ -211,7 +208,7 @@ 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.back(),
|
onPressed: () => Get.toNamed('/dashboard'),
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
MyText.titleLarge(
|
MyText.titleLarge(
|
||||||
@ -224,10 +221,9 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
/// SEARCH + FILTER BAR
|
/// Search bar and actions
|
||||||
Padding(
|
Padding(
|
||||||
padding: MySpacing.xy(8, 8),
|
padding: MySpacing.xy(8, 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -245,8 +241,9 @@ 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),
|
||||||
@ -308,10 +305,11 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
const PopupMenuItem<int>(
|
const PopupMenuItem<int>(
|
||||||
enabled: false,
|
enabled: false,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: Text("Actions",
|
child: Text(
|
||||||
|
"Actions",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
color: Colors.grey)),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuItem<int>(
|
const PopupMenuItem<int>(
|
||||||
value: 1,
|
value: 1,
|
||||||
@ -331,7 +329,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// PROJECT LIST
|
/// Project List
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user