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