diff --git a/lib/controller/service_project/service_project_screen_controller.dart b/lib/controller/service_project/service_project_screen_controller.dart index ae0b423..401b68b 100644 --- a/lib/controller/service_project/service_project_screen_controller.dart +++ b/lib/controller/service_project/service_project_screen_controller.dart @@ -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 = [].obs; - var isLoading = false.obs; - var searchQuery = ''.obs; + final projects = [].obs; + final isLoading = false.obs; + final searchQuery = ''.obs; - RxList 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 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 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; } diff --git a/lib/model/service_project/service_projects_list_model.dart b/lib/model/service_project/service_projects_list_model.dart index 5d9da34..7117113 100644 --- a/lib/model/service_project/service_projects_list_model.dart +++ b/lib/model/service_project/service_projects_list_model.dart @@ -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? data; + final List data; ProjectData({ required this.currentPage, required this.totalPages, required this.totalEntities, - this.data, + required this.data, }); factory ProjectData.fromJson(Map json) { @@ -55,7 +55,8 @@ class ProjectData { totalPages: json['totalPages'] ?? 1, totalEntities: json['totalEntites'] ?? 0, data: json['data'] != null - ? List.from(json['data'].map((x) => ProjectItem.fromJson(x))) + ? List.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 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 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.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 json) { + return Status( + id: json['id'] ?? '', + status: json['status'] ?? '', + ); + } + + Map 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 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 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 json) { + return Service( + id: json['id'] ?? '', + name: json['name'] ?? '', + description: json['description'] ?? '', + isSystem: json['isSystem'] ?? false, + isActive: json['isActive'] ?? false, + ); + } + + Map 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 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 toJson() => { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'email': email, + 'photo': photo, + 'jobRoleId': jobRoleId, + 'jobRoleName': jobRoleName, }; } diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 904d101..81b32de 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -80,14 +80,9 @@ class _EmployeeDetailPageState extends State 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 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 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 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 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 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), ); diff --git a/lib/view/finance/payment_request_detail_screen.dart b/lib/view/finance/payment_request_detail_screen.dart index 49ffcb6..91a99c2 100644 --- a/lib/view/finance/payment_request_detail_screen.dart +++ b/lib/view/finance/payment_request_detail_screen.dart @@ -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 diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart index cea61be..e9f18b9 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -20,15 +20,20 @@ class ServiceProjectDetailsScreen extends StatefulWidget { class _ServiceProjectDetailsScreenState extends State 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( diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index c4f6b7f..47311c4 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -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 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 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 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 ); } -// 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 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 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 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 ), ), ), - 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 suffixIcon: ValueListenableBuilder( 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 const PopupMenuItem( 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( value: 1, @@ -331,7 +329,7 @@ class _ServiceProjectScreenState extends State ), ), - /// PROJECT LIST + /// Project List Expanded( child: Obx(() { if (controller.isLoading.value) {