made chnages in details screen and list screen

This commit is contained in:
Vaibhav Surve 2025-11-12 16:48:53 +05:30
parent 1de5e7fae7
commit c8a8d45c66
6 changed files with 417 additions and 246 deletions

View File

@ -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;
} }

View File

@ -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,
}; };
} }

View File

@ -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,27 +90,21 @@ 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( color: Colors.grey[600],
fontSize: 12, fontWeight: 500,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
), ),
MySpacing.height(4), MySpacing.height(4),
Text( MyText(
value, value,
style: TextStyle( color: isActionable && value != 'NA'
fontSize: 15, ? Colors.blueAccent
color: isActionable && value != 'NA' : Colors.black87,
? contentTheme.primary fontWeight: 500,
: Colors.black87, decoration: isActionable && value != 'NA'
fontWeight: FontWeight.w500, ? TextDecoration.underline
decoration: isActionable && value != 'NA' : TextDecoration.none,
? TextDecoration.underline
: TextDecoration.none,
),
), ),
], ],
), ),
@ -151,16 +140,13 @@ 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: 700,
fontWeight: FontWeight.bold, color: Colors.black87,
color: Colors.black87,
),
), ),
], ],
), ),
@ -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),
); );

View File

@ -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,66 +634,68 @@ 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)
_labelValueRow("Attachment Required:", "Yes"), _labelValueRow("Attachment Required:", "Yes"),
// 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

View File

@ -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.setProjectId(widget.projectId); controller = Get.put(ServiceProjectDetailsController());
// Fetch project detail safely after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.setProjectId(widget.projectId);
});
} }
@override @override
@ -57,38 +62,33 @@ 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: 500,
fontWeight: FontWeight.w500,
),
), ),
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.blueAccent
? Colors.redAccent : Colors.black87,
: Colors.black87, fontWeight: 500,
fontWeight: FontWeight.w500, 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: 700,
fontWeight: FontWeight.bold, 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(

View File

@ -6,7 +6,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/controller/service_project/service_project_screen_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/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'; import 'package:marco/view/service_project/service_project_details_screen.dart';
class ServiceProjectScreen extends StatefulWidget { class ServiceProjectScreen extends StatefulWidget {
@ -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();
controller.fetchProjects();
// Fetch projects safely after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
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,62 +66,71 @@ 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(
style: TextStyle( "Actions",
fontWeight: FontWeight.bold, style: TextStyle(
color: Colors.grey)), fontWeight: FontWeight.bold, 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) {