From 4900ce87f70a773eb70c0a12ae956c102da6551f Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 17 Nov 2025 11:32:07 +0530 Subject: [PATCH 01/60] Rebase issues solved --- lib/helpers/services/api_endpoints.dart | 4 +-- lib/helpers/services/api_service.dart | 42 +++++++------------------ 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 4ee46e4..05ce110 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,8 +1,8 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; - static const String baseUrl = "https://devapi.marcoaiot.com/api"; + //static const String baseUrl = "https://devapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 6061bcc..e1aee7d 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -427,35 +427,22 @@ class ApiService { } /// Get details of a single service project - static Future getServiceProjectDetailApi( - String projectId) async { - final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; - logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); - try { - final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); - return null; - } - if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); + logSafe( + "Service Project Detail request failed: null response", + level: LogLevel.error, + ); return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -465,23 +452,18 @@ class ApiService { return ServiceProjectDetailModel.fromJson(jsonResponse); } } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", - level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); - } - if (jsonResponse != null) { - return ServiceProjectDetailModel.fromJson(jsonResponse); - } - } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", - level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); + logSafe( + "Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error, + ); + logSafe( + "StackTrace: $stack", + level: LogLevel.debug, + ); } return null; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({ From d229facfba7e695a550f2c149d84cba1676b8c08 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 16:48:53 +0530 Subject: [PATCH 02/60] made chnages in details screen and list screen --- .../service_project_screen_controller.dart | 49 +++- .../service_projects_list_model.dart | 231 +++++++++++++++--- .../employees/employee_detail_screen.dart | 53 ++-- .../payment_request_detail_screen.dart | 81 +++--- .../service_project_details_screen.dart | 87 ++++--- .../service_project_screen.dart | 162 ++++++------ 6 files changed, 417 insertions(+), 246 deletions(-) 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) { From 456df30c8e4b36b053437d3017b6c9ccde08df9d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 17:58:33 +0530 Subject: [PATCH 03/60] made chnages into dynamic menus --- lib/helpers/utils/permission_constants.dart | 49 +- lib/helpers/widgets/my_custom_skeleton.dart | 43 ++ lib/model/dynamicMenu/dynamic_menu_model.dart | 4 + lib/view/dashboard/dashboard_screen.dart | 457 +++++++----------- lib/view/finance/finance_screen.dart | 208 ++++---- 5 files changed, 398 insertions(+), 363 deletions(-) diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index f257cfc..733f6ab 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -25,14 +25,16 @@ class Permissions { // ------------------- Project Infrastructure -------------------------- /// Permission to manage project infrastructure (e.g., site details) - static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373"; + static const String manageProjectInfra = + "cf2825ad-453b-46aa-91d9-27c124d63373"; /// Permission to view infrastructure-related details static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"; // ------------------- Attendance Management --------------------------- /// Permission to regularize (edit/update) attendance records - static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; + static const String regularizeAttendance = + "57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; // ------------------- Task Management --------------------------------- /// Permission to create and manage tasks @@ -90,7 +92,8 @@ class Permissions { // ------------------- Application Roles ------------------------------- /// Application role ID for users with full expense management rights - static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7"; + static const String expenseManagement = + "a4e25142-449b-4334-a6e5-22f70e4732d7"; // ------------------- Document Entities ------------------------------- /// Entity ID for project documents @@ -118,3 +121,43 @@ class Permissions { /// Permission to verify documents static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0"; } + +/// Contains constants for menu item IDs fetched from the sidebar menu API. +class MenuItems { + /// Dashboard menu + static const String dashboard = "29e03eda-03e8-4714-92fa-67ae0dc53202"; + + /// Daily Task Planning menu + static const String dailyTaskPlanning = + "77ac5205-f823-442e-b9e4-2420d658aa02"; + + /// Daily Progress Report menu + static const String dailyProgressReport = + "299e3cf5-d034-4403-b4a1-ea46d2714832"; + + /// Employees menu + static const String employees = "78f0206d-c6cc-44d0-832a-2031ed203018"; + + /// Attendance menu + static const String attendance = "2f212030-f36b-456c-8e7c-11f00f9ba42b"; + + /// Directory menu + static const String directory = "31bc367b-7c58-4604-95eb-da059a384103"; + + /// Expense & Reimbursement menu + static const String expenseReimbursement = + "0f0dc1a7-1aca-4cdb-9d7a-8a769ce40728"; + + /// Payment Requests menu + static const String paymentRequests = "b350a59f-2372-4f68-8dcf-f7cfc44523ca"; + + /// Advance Payment Statements menu + static const String advancePaymentStatements = + "e0251cc1-e6d9-417a-9c76-489cc4b6c347"; + + /// Finance menu + static const String finance = "5ac409dd-bbe0-4d56-bcb9-229bd3a6353c"; + + /// Documents menu + static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3"; +} diff --git a/lib/helpers/widgets/my_custom_skeleton.dart b/lib/helpers/widgets/my_custom_skeleton.dart index b81ee63..5bb5309 100644 --- a/lib/helpers/widgets/my_custom_skeleton.dart +++ b/lib/helpers/widgets/my_custom_skeleton.dart @@ -33,6 +33,48 @@ class SkeletonLoaders { ); } +// Inside SkeletonLoaders class + static Widget dashboardCardsSkeleton({double? maxWidth}) { + return LayoutBuilder(builder: (context, constraints) { + double width = maxWidth ?? constraints.maxWidth; + int crossAxisCount = (width ~/ 80).clamp(2, 4); + double cardWidth = (width - (crossAxisCount - 1) * 6) / crossAxisCount; + + return Wrap( + spacing: 6, + runSpacing: 6, + children: List.generate(6, (index) { + return MyCard.bordered( + width: cardWidth, + height: 60, + paddingAll: 4, + borderRadiusAll: 5, + border: Border.all(color: Colors.grey.withOpacity(0.15)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(4), + ), + ), + MySpacing.height(4), + Container( + width: cardWidth * 0.5, + height: 10, + color: Colors.grey.shade300, + ), + ], + ), + ); + }), + ); + }); + } + // Inside SkeletonLoaders class static Widget paymentRequestListSkeletonLoader() { return ListView.separated( @@ -256,6 +298,7 @@ class SkeletonLoaders { ), ); } + // Employee Detail Skeleton Loader static Widget employeeDetailSkeletonLoader() { return SingleChildScrollView( diff --git a/lib/model/dynamicMenu/dynamic_menu_model.dart b/lib/model/dynamicMenu/dynamic_menu_model.dart index 8348447..b70b699 100644 --- a/lib/model/dynamicMenu/dynamic_menu_model.dart +++ b/lib/model/dynamicMenu/dynamic_menu_model.dart @@ -59,11 +59,13 @@ class MenuItem { final String id; // Unique item ID final String name; // Display text final bool available; // Availability flag + final String mobileLink; // Mobile navigation link MenuItem({ required this.id, required this.name, required this.available, + required this.mobileLink, }); /// Creates MenuItem from JSON map @@ -72,6 +74,7 @@ class MenuItem { id: json['id'] as String? ?? '', name: json['name'] as String? ?? '', available: json['available'] as bool? ?? false, + mobileLink: json['mobileLink'] as String? ?? '', ); } @@ -81,6 +84,7 @@ class MenuItem { 'id': id, 'name': name, 'available': available, + 'mobileLink': mobileLink, }; } } diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index b8c1402..6ff69e3 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -3,37 +3,24 @@ import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:get/get.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/controller/project_controller.dart'; +import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_card.dart'; -import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart'; import 'package:marco/helpers/widgets/dashbaord/project_progress_chart.dart'; -import 'package:marco/view/layouts/layout.dart'; -import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; -import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart'; import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart'; import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart'; import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart'; +import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart'; +import 'package:marco/view/layouts/layout.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); - static const String dashboardRoute = "/dashboard"; - static const String employeesRoute = "/dashboard/employees"; - static const String projectsRoute = "/dashboard"; - static const String attendanceRoute = "/dashboard/attendance"; - static const String tasksRoute = "/dashboard/daily-task"; - static const String dailyTasksRoute = "/dashboard/daily-task-Planning"; - static const String dailyTasksProgressRoute = - "/dashboard/daily-task-progress"; - static const String directoryMainPageRoute = "/dashboard/directory-main-page"; - static const String financeMainPageRoute = "/dashboard/finance"; - static const String documentMainPageRoute = "/dashboard/document-main-page"; - static const String serviceprojectsRoute = "/dashboard/service-projects"; - @override State createState() => _DashboardScreenState(); } @@ -42,6 +29,7 @@ class _DashboardScreenState extends State with UIMixin { final DashboardController dashboardController = Get.put(DashboardController(), permanent: true); final DynamicMenuController menuController = Get.put(DynamicMenuController()); + final ProjectController projectController = Get.find(); bool hasMpin = true; @@ -60,11 +48,11 @@ class _DashboardScreenState extends State with UIMixin { Widget build(BuildContext context) { return Layout( child: SingleChildScrollView( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildDashboardStats(context), + _buildDashboardCards(), MySpacing.height(24), _buildAttendanceChartSection(), MySpacing.height(24), @@ -80,13 +68,9 @@ class _DashboardScreenState extends State with UIMixin { child: DashboardOverviewWidgets.tasksOverview(), ), MySpacing.height(24), - ExpenseByStatusWidget(controller: dashboardController), MySpacing.height(24), - - // Expense Type Report Chart ExpenseTypeReportChart(), - MySpacing.height(24), MonthlyExpenseDashboardChart(), ], @@ -95,7 +79,162 @@ class _DashboardScreenState extends State with UIMixin { ); } - /// Project Progress Chart Section + /// ---------------- Dynamic Dashboard Cards ---------------- + Widget _buildDashboardCards() { + return Obx(() { + if (menuController.isLoading.value) { + return SkeletonLoaders.dashboardCardsSkeleton(); + } + + if (menuController.hasError.value || menuController.menuItems.isEmpty) { + return const Center( + child: Text( + "Failed to load menus. Please try again later.", + style: TextStyle(color: Colors.red), + ), + ); + } + + final projectSelected = projectController.selectedProject != null; + + // Define dashboard card meta with order + final List cardOrder = [ + MenuItems.attendance, + MenuItems.employees, + MenuItems.dailyTaskPlanning, + MenuItems.dailyProgressReport, + MenuItems.directory, + MenuItems.finance, + MenuItems.documents, + ]; + + final Map cardMeta = { + MenuItems.attendance: + _DashboardCardMeta(LucideIcons.scan_face, contentTheme.success), + MenuItems.employees: + _DashboardCardMeta(LucideIcons.users, contentTheme.warning), + MenuItems.dailyTaskPlanning: + _DashboardCardMeta(LucideIcons.logs, contentTheme.info), + MenuItems.dailyProgressReport: + _DashboardCardMeta(LucideIcons.list_todo, contentTheme.info), + MenuItems.directory: + _DashboardCardMeta(LucideIcons.folder, contentTheme.info), + MenuItems.finance: + _DashboardCardMeta(LucideIcons.wallet, contentTheme.info), + MenuItems.documents: + _DashboardCardMeta(LucideIcons.file_text, contentTheme.info), + }; + + // Filter only available menus that exist in cardMeta + final allowedMenusMap = { + for (var menu in menuController.menuItems) + if (menu.available && cardMeta.containsKey(menu.id)) menu.id: menu + }; + + if (allowedMenusMap.isEmpty) { + return const Center( + child: Text( + "No accessible modules found.", + style: TextStyle(color: Colors.grey), + ), + ); + } + + // Create list of cards in fixed order + final stats = + cardOrder.where((id) => allowedMenusMap.containsKey(id)).map((id) { + final menu = allowedMenusMap[id]!; + final meta = cardMeta[id]!; + return _DashboardStatItem( + meta.icon, menu.name, meta.color, menu.mobileLink); + }).toList(); + + return LayoutBuilder(builder: (context, constraints) { + int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4); + double cardWidth = + (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount; + + return Wrap( + spacing: 6, + runSpacing: 6, + alignment: WrapAlignment.start, + children: stats + .map((stat) => + _buildDashboardCard(stat, projectSelected, cardWidth)) + .toList(), + ); + }); + }); + } + + Widget _buildDashboardCard( + _DashboardStatItem stat, bool isProjectSelected, double width) { + final isEnabled = stat.title == "Attendance" ? true : isProjectSelected; + + return Opacity( + opacity: isEnabled ? 1.0 : 0.4, + child: IgnorePointer( + ignoring: !isEnabled, + child: InkWell( + onTap: () => _onDashboardCardTap(stat, isEnabled), + borderRadius: BorderRadius.circular(5), + child: MyCard.bordered( + width: width, + height: 60, + paddingAll: 4, + borderRadiusAll: 5, + border: Border.all(color: Colors.grey.withOpacity(0.15)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: stat.color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + stat.icon, + size: 16, + color: stat.color, + ), + ), + MySpacing.height(4), + Flexible( + child: Text( + stat.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 10, + overflow: TextOverflow.ellipsis, + ), + maxLines: 2, + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _onDashboardCardTap(_DashboardStatItem statItem, bool isEnabled) { + if (!isEnabled) { + Get.defaultDialog( + title: "No Project Selected", + middleText: "Please select a project before accessing this module.", + confirm: ElevatedButton( + onPressed: () => Get.back(), + child: const Text("OK"), + ), + ); + } else { + Get.toNamed(statItem.route); + } + } + + /// ---------------- Project Progress Chart ---------------- Widget _buildProjectProgressChartSection() { return Obx(() { if (dashboardController.projectChartData.isEmpty) { @@ -119,267 +258,45 @@ class _DashboardScreenState extends State with UIMixin { }); } - /// Attendance Chart Section + /// ---------------- Attendance Chart ---------------- Widget _buildAttendanceChartSection() { return Obx(() { - final isAttendanceAllowed = menuController.isMenuAllowed("Attendance"); - - if (!isAttendanceAllowed) { - // 🚫 Don't render anything if attendance menu is not allowed + final attendanceMenu = menuController.menuItems + .firstWhereOrNull((m) => m.id == MenuItems.attendance); + if (attendanceMenu == null || !attendanceMenu.available) return const SizedBox.shrink(); - } - return GetBuilder( - id: 'dashboard_controller', - builder: (projectController) { - final isProjectSelected = projectController.selectedProject != null; - return Opacity( - opacity: isProjectSelected ? 1.0 : 0.4, - child: IgnorePointer( - ignoring: !isProjectSelected, - child: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: SizedBox( - height: 400, - child: AttendanceDashboardChart(), - ), - ), - ), - ); - }, - ); - }); - } - - /// No Project Assigned Message - Widget _buildNoProjectMessage() { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: MyCard( - color: Colors.orange.withOpacity(0.1), - paddingAll: 12, - child: Row( - children: [ - const Icon(Icons.info_outline, color: Colors.orange), - MySpacing.width(8), - Expanded( - child: MyText.bodySmall( - "No projects assigned yet. Please contact your manager to get started.", - color: Colors.orange.shade800, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ); - } - - /// Loading Skeletons - Widget _buildLoadingSkeleton(BuildContext context) { - return Wrap( - spacing: 10, - runSpacing: 10, - children: List.generate( - 4, - (index) => - _buildStatCardSkeleton(MediaQuery.of(context).size.width / 3), - ), - ); - } - - /// Skeleton Card - Widget _buildStatCardSkeleton(double width) { - return MyCard.bordered( - width: width, - height: 100, - paddingAll: 5, - borderRadiusAll: 5, - border: Border.all(color: Colors.grey.withOpacity(0.15)), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyContainer.rounded( - paddingAll: 12, - color: Colors.grey.shade300, - child: const SizedBox(width: 18, height: 18), - ), - MySpacing.height(8), - Container( - height: 12, - width: 60, - color: Colors.grey.shade300, - ), - ], - ), - ); - } - - /// Dashboard Statistics Section - Widget _buildDashboardStats(BuildContext context) { - return Obx(() { - if (menuController.isLoading.value) { - return _buildLoadingSkeleton(context); - } - - if (menuController.hasError.value || menuController.menuItems.isEmpty) { - return Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: MyText.bodySmall( - "Failed to load menus. Please try again later.", - color: Colors.red, - ), - ), - ); - } - - final projectController = Get.find(); final isProjectSelected = projectController.selectedProject != null; - // Keep previous stat items (icons, title, routes) - final stats = [ - _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, - DashboardScreen.attendanceRoute), - _StatItem(LucideIcons.users, "Employees", contentTheme.warning, - DashboardScreen.employeesRoute), - _StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info, - DashboardScreen.dailyTasksRoute), - _StatItem(LucideIcons.list_todo, "Daily Progress Report", - contentTheme.info, DashboardScreen.dailyTasksProgressRoute), - _StatItem(LucideIcons.folder, "Directory", contentTheme.info, - DashboardScreen.directoryMainPageRoute), - _StatItem(LucideIcons.wallet, "Finance", contentTheme.info, - DashboardScreen.financeMainPageRoute), - _StatItem(LucideIcons.file_text, "Documents", contentTheme.info, - DashboardScreen.documentMainPageRoute), - _StatItem(LucideIcons.briefcase, "Service Projects", contentTheme.info, - DashboardScreen.serviceprojectsRoute), - ]; - - // Safe menu check function to avoid exceptions - bool _isMenuAllowed(String menuTitle) { - try { - return menuController.menuItems.isNotEmpty - ? menuController.isMenuAllowed(menuTitle) - : false; - } catch (e) { - return false; - } - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isProjectSelected) _buildNoProjectMessage(), - LayoutBuilder( - builder: (context, constraints) { - int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 8); - double cardWidth = - (constraints.maxWidth - (crossAxisCount - 1) * 6) / - crossAxisCount; - - return Wrap( - spacing: 6, - runSpacing: 6, - alignment: WrapAlignment.start, - children: stats - .where((stat) => - stat.title == "Service Projects" || - _isMenuAllowed(stat.title)) - .map((stat) => - _buildStatCard(stat, isProjectSelected, cardWidth)) - .toList(), - ); - }, - ), - ], - ); - }); - } - - /// Stat Card (Compact + Small) - Widget _buildStatCard( - _StatItem statItem, bool isProjectSelected, double width) { - const double cardHeight = 60; - - final bool isEnabled = statItem.title == "Attendance" || isProjectSelected; - - return Opacity( - opacity: isEnabled ? 1.0 : 0.4, - child: IgnorePointer( - ignoring: !isEnabled, - child: InkWell( - onTap: () => _handleStatCardTap(statItem, isEnabled), - borderRadius: BorderRadius.circular(5), - child: MyCard.bordered( - width: width, - height: cardHeight, - paddingAll: 4, - borderRadiusAll: 5, - border: Border.all(color: Colors.grey.withOpacity(0.15)), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildStatCardIconCompact(statItem, size: 12), - MySpacing.height(4), - Flexible( - child: Text( - statItem.title, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 8, - overflow: TextOverflow.visible, - ), - maxLines: 2, - softWrap: true, - ), - ), - ], + return Opacity( + opacity: isProjectSelected ? 1.0 : 0.4, + child: IgnorePointer( + ignoring: !isProjectSelected, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: SizedBox( + height: 400, + child: AttendanceDashboardChart(), ), ), ), - ), - ); - } - - /// Compact Icon (smaller) - Widget _buildStatCardIconCompact(_StatItem statItem, {double size = 12}) { - return MyContainer.rounded( - paddingAll: 4, - color: statItem.color.withOpacity(0.1), - child: Icon( - statItem.icon, - size: size, - color: statItem.color, - ), - ); - } - - /// Handle Tap - void _handleStatCardTap(_StatItem statItem, bool isEnabled) { - if (!isEnabled) { - Get.defaultDialog( - title: "No Project Selected", - middleText: - "You need to select a project before accessing this section.", - confirm: ElevatedButton( - onPressed: () => Get.back(), - child: const Text("OK"), - ), ); - } else { - Get.toNamed(statItem.route); - } + }); } } -class _StatItem { +/// ---------------- Dashboard Card Models ---------------- +class _DashboardStatItem { final IconData icon; final String title; final Color color; final String route; - _StatItem(this.icon, this.title, this.color, this.route); + _DashboardStatItem(this.icon, this.title, this.color, this.route); +} + +class _DashboardCardMeta { + final IconData icon; + final Color color; + _DashboardCardMeta(this.icon, this.color); } diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index c60bc64..eaa1508 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -11,6 +11,8 @@ import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart'; import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart'; import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; +import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart'; class FinanceScreen extends StatefulWidget { const FinanceScreen({super.key}); @@ -27,6 +29,7 @@ class _FinanceScreenState extends State late Animation _fadeAnimation; final DashboardController dashboardController = Get.put(DashboardController(), permanent: true); + @override void initState() { super.initState(); @@ -117,8 +120,7 @@ class _FinanceScreenState extends State return const Center(child: CircularProgressIndicator()); } - if (menuController.hasError.value || - menuController.menuItems.isEmpty) { + if (menuController.hasError.value || menuController.menuItems.isEmpty) { return const Center( child: Text( "Failed to load menus. Please try again later.", @@ -127,10 +129,18 @@ class _FinanceScreenState extends State ); } - // ✅ Only allow finance cards if "Expense" menu is allowed - final isExpenseAllowed = menuController.isMenuAllowed("Expense & Reimbursement"); + // Filter allowed Finance menus dynamically + final financeMenuIds = [ + MenuItems.expenseReimbursement, + MenuItems.paymentRequests, + MenuItems.advancePaymentStatements, + ]; - if (!isExpenseAllowed) { + final financeMenus = menuController.menuItems + .where((m) => financeMenuIds.contains(m.id) && m.available) + .toList(); + + if (financeMenus.isEmpty) { return const Center( child: Text( "You don’t have access to the Finance section.", @@ -143,7 +153,7 @@ class _FinanceScreenState extends State padding: const EdgeInsets.all(16), child: Column( children: [ - _buildFinanceModulesCompact(), + _buildFinanceModulesCompact(financeMenus), MySpacing.height(24), ExpenseByStatusWidget(controller: dashboardController), MySpacing.height(24), @@ -159,103 +169,115 @@ class _FinanceScreenState extends State } // --- Finance Modules (Compact Dashboard-style) --- - Widget _buildFinanceModulesCompact() { - final stats = [ - _FinanceStatItem(LucideIcons.badge_dollar_sign, "Expense & Reimbursement", - contentTheme.info, "/dashboard/expense-main-page"), - _FinanceStatItem(LucideIcons.receipt_text, "Payment Request", - contentTheme.primary, "/dashboard/payment-request"), - _FinanceStatItem(LucideIcons.wallet, "Advance Payment", - contentTheme.warning, "/dashboard/advance-payment"), - ]; +Widget _buildFinanceModulesCompact(List financeMenus) { + // Map menu IDs to icon + color + final Map financeCardMeta = { + MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info), + MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary), + MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning), + }; - final projectSelected = projectController.selectedProject != null; + // Build the stat items using API-provided mobileLink + final stats = financeMenus.map((menu) { + final meta = financeCardMeta[menu.id]!; - return LayoutBuilder(builder: (context, constraints) { - int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4); - double cardWidth = - (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount; + // --- Log the routing info --- + debugPrint( + "[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}"); - return Wrap( - spacing: 6, - runSpacing: 6, - alignment: WrapAlignment.end, - children: stats - .map((stat) => - _buildFinanceModuleCard(stat, projectSelected, cardWidth)) - .toList(), - ); - }); - } + return _FinanceStatItem( + meta.icon, + menu.name, + meta.color, + menu.mobileLink, // Each card navigates to its own route + ); + }).toList(); - Widget _buildFinanceModuleCard( - _FinanceStatItem stat, bool isProjectSelected, double width) { - final bool isEnabled = isProjectSelected; + final projectSelected = projectController.selectedProject != null; - return Opacity( - opacity: isEnabled ? 1.0 : 0.4, - child: IgnorePointer( - ignoring: !isEnabled, - child: InkWell( - onTap: () => _onCardTap(stat, isEnabled), - borderRadius: BorderRadius.circular(5), - child: MyCard.bordered( - width: width, - height: 60, - paddingAll: 4, - borderRadiusAll: 5, - border: Border.all(color: Colors.grey.withOpacity(0.15)), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: stat.color.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - stat.icon, - size: 16, - color: stat.color, - ), + return LayoutBuilder(builder: (context, constraints) { + // Determine number of columns dynamically + int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4); + double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount; + + return Wrap( + spacing: 6, + runSpacing: 6, + alignment: WrapAlignment.end, + children: stats + .map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth)) + .toList(), + ); + }); +} + +Widget _buildFinanceModuleCard( + _FinanceStatItem stat, bool isProjectSelected, double width) { + return Opacity( + opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected + child: IgnorePointer( + ignoring: !isProjectSelected, + child: InkWell( + onTap: () => _onCardTap(stat, isProjectSelected), + borderRadius: BorderRadius.circular(5), + child: MyCard.bordered( + width: width, + height: 60, + paddingAll: 4, + borderRadiusAll: 5, + border: Border.all(color: Colors.grey.withOpacity(0.15)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: stat.color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), ), - MySpacing.height(4), - Flexible( - child: Text( - stat.title, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 10, - overflow: TextOverflow.ellipsis, - ), - maxLines: 2, - softWrap: true, - ), + child: Icon( + stat.icon, + size: 16, + color: stat.color, ), - ], - ), + ), + MySpacing.height(4), + Flexible( + child: Text( + stat.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 10, + overflow: TextOverflow.ellipsis, + ), + maxLines: 2, + softWrap: true, + ), + ), + ], ), ), ), - ); - } + ), + ); +} - void _onCardTap(_FinanceStatItem statItem, bool isEnabled) { - if (!isEnabled) { - Get.defaultDialog( - title: "No Project Selected", - middleText: "Please select a project before accessing this section.", - confirm: ElevatedButton( - onPressed: () => Get.back(), - child: const Text("OK"), - ), - ); - } else { - Get.toNamed(statItem.route); - } +void _onCardTap(_FinanceStatItem statItem, bool isEnabled) { + if (!isEnabled) { + Get.defaultDialog( + title: "No Project Selected", + middleText: "Please select a project before accessing this section.", + confirm: ElevatedButton( + onPressed: () => Get.back(), + child: const Text("OK"), + ), + ); + } else { + // Navigate to the card's specific route + Get.toNamed(statItem.route); } } + } class _FinanceStatItem { final IconData icon; @@ -265,3 +287,9 @@ class _FinanceStatItem { _FinanceStatItem(this.icon, this.title, this.color, this.route); } + +class _FinanceCardMeta { + final IconData icon; + final Color color; + _FinanceCardMeta(this.icon, this.color); +} From 02c10d8115c26123911aab932fe8247c7aff0800 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 13 Nov 2025 12:26:39 +0530 Subject: [PATCH 04/60] corrected the payment proceed validation --- .../expense/reimbursement_bottom_sheet.dart | 31 +++++++++++++------ ...ent_request_rembursement_bottom_sheet.dart | 14 +++------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/lib/model/expense/reimbursement_bottom_sheet.dart b/lib/model/expense/reimbursement_bottom_sheet.dart index c2d235c..6ca4d81 100644 --- a/lib/model/expense/reimbursement_bottom_sheet.dart +++ b/lib/model/expense/reimbursement_bottom_sheet.dart @@ -197,22 +197,33 @@ class _ReimbursementBottomSheetState extends State { return; } - if (expenseTransactionDate != null && - selectedDate.isBefore(expenseTransactionDate)) { - showAppSnackbar( - title: "Invalid Date", - message: - "Reimbursement date cannot be before the transaction date (${DateFormat('yyyy-MM-dd').format(expenseTransactionDate)}).", - type: SnackbarType.warning, + if (expenseTransactionDate != null && selectedDate != null) { + final normalizedSelected = DateTime( + selectedDate.year, + selectedDate.month, + selectedDate.day, ); - return; + final normalizedTransaction = DateTime( + expenseTransactionDate.year, + expenseTransactionDate.month, + expenseTransactionDate.day, + ); + + if (normalizedSelected.isBefore(normalizedTransaction)) { + showAppSnackbar( + title: "Invalid Date", + message: + "Reimbursement date cannot be before the transaction date (${DateFormat('yyyy-MM-dd').format(expenseTransactionDate)}).", + type: SnackbarType.warning, + ); + return; + } } } final success = await widget.onSubmit( comment: commentCtrl.text.trim(), - reimburseTransactionId: txnCtrl.text - .trim(), + reimburseTransactionId: txnCtrl.text.trim(), reimburseDate: dateStr.value, reimburseById: controller.selectedReimbursedBy.value!.id, statusId: widget.statusId, diff --git a/lib/model/finance/payment_request_rembursement_bottom_sheet.dart b/lib/model/finance/payment_request_rembursement_bottom_sheet.dart index 0c39ca6..f8f6983 100644 --- a/lib/model/finance/payment_request_rembursement_bottom_sheet.dart +++ b/lib/model/finance/payment_request_rembursement_bottom_sheet.dart @@ -170,10 +170,11 @@ class _UpdatePaymentRequestWithReimbursementState dateStr.value.isEmpty || baseAmountCtrl.text.trim().isEmpty || taxAmountCtrl.text.trim().isEmpty || - commentCtrl.text.trim().isEmpty) { + commentCtrl.text.trim().isEmpty || + controller.selectedReimbursedBy.value == null) { showAppSnackbar( title: "Incomplete", - message: "Please fill all mandatory fields", + message: "Please fill all mandatory fields, including 'Paid By'", type: SnackbarType.warning, ); return; @@ -233,7 +234,6 @@ class _UpdatePaymentRequestWithReimbursementState decoration: _inputDecoration("Enter transaction ID"), ), MySpacing.height(16), - _requiredLabel("Transaction Date"), MySpacing.height(8), GestureDetector( @@ -266,8 +266,7 @@ class _UpdatePaymentRequestWithReimbursementState ), ), MySpacing.height(16), - - MyText.labelMedium("Paid By (Optional)"), + _requiredLabel("Paid By"), MySpacing.height(8), GestureDetector( onTap: _showEmployeeList, @@ -294,7 +293,6 @@ class _UpdatePaymentRequestWithReimbursementState ), ), MySpacing.height(16), - _requiredLabel("Base Amount"), MySpacing.height(8), TextField( @@ -303,7 +301,6 @@ class _UpdatePaymentRequestWithReimbursementState decoration: _inputDecoration("Enter Base Amount"), ), MySpacing.height(16), - _requiredLabel("GST Amount"), MySpacing.height(8), TextField( @@ -312,7 +309,6 @@ class _UpdatePaymentRequestWithReimbursementState decoration: _inputDecoration("Enter GST Amount"), ), MySpacing.height(16), - MyText.labelMedium("TDS Percent"), MySpacing.height(8), TextField( @@ -335,7 +331,6 @@ class _UpdatePaymentRequestWithReimbursementState color: Colors.grey.shade600, ), MySpacing.height(16), - Obx(() => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -353,7 +348,6 @@ class _UpdatePaymentRequestWithReimbursementState ], )), MySpacing.height(20), - _requiredLabel("Comment"), MySpacing.height(8), TextField( From 674a9c691b5e2f9649bb6db969d197675b5968b1 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 14 Nov 2025 15:35:41 +0530 Subject: [PATCH 05/60] created new emploee selector bottomsheet --- .../add_service_project_job_controller.dart | 118 ++++++++ ...ice_project_details_screen_controller.dart | 88 +++++- lib/helpers/services/api_endpoints.dart | 3 + lib/helpers/services/api_service.dart | 169 +++++++++-- lib/helpers/utils/permission_constants.dart | 3 + lib/helpers/widgets/date_range_picker.dart | 24 +- lib/helpers/widgets/my_snackbar.dart | 2 +- lib/model/employees/employee_model.dart | 10 + .../multiple_select_bottomsheet.dart | 159 ++++++++++ .../add_service_project_job_bottom_sheet.dart | 241 +++++++++++++++ lib/model/service_project/job_list_model.dart | 237 +++++++++++++++ lib/view/dashboard/dashboard_screen.dart | 3 + .../service_project_details_screen.dart | 279 +++++++++++++++--- .../service_project_screen.dart | 40 ++- 14 files changed, 1289 insertions(+), 87 deletions(-) create mode 100644 lib/controller/service_project/add_service_project_job_controller.dart create mode 100644 lib/model/employees/multiple_select_bottomsheet.dart create mode 100644 lib/model/service_project/add_service_project_job_bottom_sheet.dart create mode 100644 lib/model/service_project/job_list_model.dart diff --git a/lib/controller/service_project/add_service_project_job_controller.dart b/lib/controller/service_project/add_service_project_job_controller.dart new file mode 100644 index 0000000..6195dab --- /dev/null +++ b/lib/controller/service_project/add_service_project_job_controller.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class AddServiceProjectJobController extends GetxController { + // Form Controllers + final titleCtrl = TextEditingController(); + final descCtrl = TextEditingController(); + final tagCtrl = TextEditingController(); + final FocusNode searchFocusNode = FocusNode(); + final RxBool showEmployeePicker = true.obs; + + // Observables + final startDate = Rx(DateTime.now()); + final dueDate = Rx(DateTime.now().add(const Duration(days: 1))); + final enteredTags = [].obs; + + final employees = [].obs; + final selectedAssignees = [].obs; + final isSearchingEmployees = false.obs; + + // Loading states + final isLoading = false.obs; + final isAllEmployeeLoading = false.obs; + final allEmployees = [].obs; + final employeeSearchResults = [].obs; + + @override + void onInit() { + super.onInit(); + searchEmployees(""); // pass empty string safely + } + + @override + void onClose() { + titleCtrl.dispose(); + descCtrl.dispose(); + tagCtrl.dispose(); + super.onClose(); + } + + Future searchEmployees(String query) async { + if (query.trim().isEmpty) { + employeeSearchResults.clear(); + return; + } + isSearchingEmployees.value = true; + try { + final data = + await ApiService.searchEmployeesBasic(searchString: query.trim()); + if (data is List) { + employeeSearchResults.assignAll( + data + .map((e) => EmployeeModel.fromJson(e as Map)) + .toList(), + ); + } else { + employeeSearchResults.clear(); + } + } catch (e) { + logSafe("Error searching employees: $e", level: LogLevel.error); + employeeSearchResults.clear(); + } finally { + isSearchingEmployees.value = false; + } + } + + /// Toggle employee selection + void toggleAssignee(EmployeeModel employee) { + if (selectedAssignees.contains(employee)) { + selectedAssignees.remove(employee); + } else { + selectedAssignees.add(employee); + } + } + + /// Create Service Project Job API call + Future createJob(String projectId) async { + if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) { + showAppSnackbar( + title: "Validation", + message: "Title and Description are required", + type: SnackbarType.warning, + ); + return; + } + + final assigneeIds = selectedAssignees.map((e) => e.id).toList(); + + final success = await ApiService.createServiceProjectJobApi( + title: titleCtrl.text.trim(), + description: descCtrl.text.trim(), + projectId: projectId, + assignees: assigneeIds.map((id) => {"id": id}).toList(), + startDate: startDate.value!, + dueDate: dueDate.value!, + tags: enteredTags.map((tag) => {"name": tag}).toList(), + ); + + if (success) { + Get.back(); + showAppSnackbar( + title: "Success", + message: "Job created successfully", + type: SnackbarType.success, + ); + } else { + showAppSnackbar( + title: "Error", + message: "Failed to create job", + type: SnackbarType.error, + ); + } + } +} diff --git a/lib/controller/service_project/service_project_details_screen_controller.dart b/lib/controller/service_project/service_project_details_screen_controller.dart index 65b9f38..2761723 100644 --- a/lib/controller/service_project/service_project_details_screen_controller.dart +++ b/lib/controller/service_project/service_project_details_screen_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/service_project/service_projects_details_model.dart'; +import 'package:marco/model/service_project/job_list_model.dart'; class ServiceProjectDetailsController extends GetxController { // Selected project id @@ -9,19 +10,39 @@ class ServiceProjectDetailsController extends GetxController { // Project details var projectDetail = Rxn(); - // Loading state + // Job list + var jobList = [].obs; + + // Loading states var isLoading = false.obs; + var isJobLoading = false.obs; - // Error message + // Error messages var errorMessage = ''.obs; + var jobErrorMessage = ''.obs; - /// Set project id and fetch its details + // Pagination + var pageNumber = 1; + final int pageSize = 20; + var hasMoreJobs = true.obs; + + @override + void onInit() { + super.onInit(); + // Fetch job list initially even if projectId is empty + fetchProjectJobs(initialLoad: true); + } + + /// Set project id and fetch its details + jobs void setProjectId(String id) { projectId.value = id; fetchProjectDetail(); + pageNumber = 1; + hasMoreJobs.value = true; + fetchProjectJobs(initialLoad: true); } - /// Fetch project detail from API + /// Fetch project detail Future fetchProjectDetail() async { if (projectId.value.isEmpty) { errorMessage.value = "Invalid project ID"; @@ -32,12 +53,14 @@ class ServiceProjectDetailsController extends GetxController { errorMessage.value = ''; try { - final result = await ApiService.getServiceProjectDetailApi(projectId.value); + final result = + await ApiService.getServiceProjectDetailApi(projectId.value); if (result != null && result.data != null) { projectDetail.value = result.data!; } else { - errorMessage.value = result?.message ?? "Failed to fetch project details"; + errorMessage.value = + result?.message ?? "Failed to fetch project details"; } } catch (e) { errorMessage.value = "Error: $e"; @@ -46,8 +69,57 @@ class ServiceProjectDetailsController extends GetxController { } } - /// Refresh project details manually + /// Fetch project job list + Future fetchProjectJobs({bool initialLoad = false}) async { + if (projectId.value.isEmpty && !initialLoad) { + jobErrorMessage.value = "Invalid project ID"; + return; + } + + if (!hasMoreJobs.value && !initialLoad) return; + + isJobLoading.value = true; + jobErrorMessage.value = ''; + + try { + final result = await ApiService.getServiceProjectJobListApi( + projectId: "", + pageNumber: pageNumber, + pageSize: pageSize, + isActive: true, + ); + + if (result != null && result.data != null) { + if (initialLoad) { + jobList.value = result.data?.data ?? []; + } else { + jobList.addAll(result.data?.data ?? []); + } + + hasMoreJobs.value = (result.data?.data?.length ?? 0) == pageSize; + if (hasMoreJobs.value) pageNumber++; + } else { + jobErrorMessage.value = result?.message ?? "Failed to fetch job list"; + } + } catch (e) { + jobErrorMessage.value = "Error fetching jobs: $e"; + } finally { + isJobLoading.value = false; + } + } + + /// Fetch more jobs for pagination + Future fetchMoreJobs() async { + await fetchProjectJobs(); + } + + /// Manual refresh Future refresh() async { - await fetchProjectDetail(); + pageNumber = 1; + hasMoreJobs.value = true; + await Future.wait([ + fetchProjectDetail(), + fetchProjectJobs(initialLoad: true), + ]); } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index e73e150..35e7831 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -133,4 +133,7 @@ class ApiEndpoints { // Service Project Module API Endpoints static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; + static const String getServiceProjectJobList = "/serviceproject/job/list"; + static const String getServiceProjectJobDetail = "/serviceproject/job/details"; + static const String createServiceProjectJob = "/serviceproject/job/create"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 48e5179..a5e2219 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -35,6 +35,7 @@ import 'package:marco/model/finance/payment_request_details_model.dart'; import 'package:marco/model/finance/advance_payment_model.dart'; import 'package:marco/model/service_project/service_projects_list_model.dart'; import 'package:marco/model/service_project/service_projects_details_model.dart'; +import 'package:marco/model/service_project/job_list_model.dart'; class ApiService { static const bool enableLogs = true; @@ -306,34 +307,156 @@ class ApiService { // Service Project Module APIs - /// Get details of a single service project -static Future getServiceProjectDetailApi(String projectId) async { - final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; - logSafe("Fetching details for Service Project ID: $projectId"); + /// Create a new Service Project Job + static Future createServiceProjectJobApi({ + required String title, + required String description, + required String projectId, + required List> assignees, + required DateTime startDate, + required DateTime dueDate, + required List> tags, + }) async { + const endpoint = ApiEndpoints.createServiceProjectJob; + logSafe("Creating Service Project Job for projectId: $projectId"); - try { - final response = await _getRequest(endpoint); + final body = { + "title": title, + "description": description, + "projectId": projectId, + "assignees": assignees, + "startDate": startDate.toIso8601String(), + "dueDate": dueDate.toIso8601String(), + "tags": tags, + }; - if (response == null) { - logSafe("Service Project Detail request failed: null response", level: LogLevel.error); - return null; + try { + final response = await _postRequest(endpoint, body); + + if (response == null) { + logSafe("Create Service Project Job failed: null response", + level: LogLevel.error); + return false; + } + + logSafe( + "Create Service Project Job response status: ${response.statusCode}"); + logSafe("Create Service Project Job response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Service Project Job created successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to create Service Project Job: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during createServiceProjectJobApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; } - - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); - - if (jsonResponse != null) { - return ServiceProjectDetailModel.fromJson(jsonResponse); - } - } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); } - return null; -} + /// Get Service Project Job List + static Future getServiceProjectJobListApi({ + required String projectId, + int pageNumber = 1, + int pageSize = 20, + bool isActive = true, + }) async { + const endpoint = ApiEndpoints.getServiceProjectJobList; + logSafe("Fetching Job List for Service Project: $projectId"); + + try { + final queryParams = { + 'projectId': projectId, + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + 'isActive': isActive.toString(), + }; + + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe("Service Project Job List request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Job List", + ); + + if (jsonResponse != null) { + return JobResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectJobListApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + +// API to get all employees from basic + static Future?> allEmployeesBasic({ + bool allEmployee = true, + }) async { + final queryParams = {}; + + // Always include allEmployee parameter + queryParams['allEmployee'] = allEmployee.toString(); + + final response = await _getRequest( + ApiEndpoints.getEmployeesWithoutPermission, + queryParams: queryParams, + ); + + if (response != null) { + return _parseResponse(response, label: ' All Employees Basic'); + } + + return null; + } + + /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Service Project Detail request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); + + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } /// Get Service Project List static Future getServiceProjectsListApi({ diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index 733f6ab..a5e9036 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -160,4 +160,7 @@ class MenuItems { /// Documents menu static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3"; + + /// Service Projects + static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b"; } diff --git a/lib/helpers/widgets/date_range_picker.dart b/lib/helpers/widgets/date_range_picker.dart index 6afab60..56b38e6 100644 --- a/lib/helpers/widgets/date_range_picker.dart +++ b/lib/helpers/widgets/date_range_picker.dart @@ -33,11 +33,15 @@ class _DateRangePickerWidgetState extends State ? widget.startDate.value ?? DateTime.now() : widget.endDate.value ?? DateTime.now(); + // Ensure initialDate is within firstDate..lastDate + final first = DateTime(2000); + final last = current.isAfter(DateTime.now()) ? current : DateTime.now(); + final DateTime? picked = await showDatePicker( context: context, initialDate: current, - firstDate: DateTime(2000), - lastDate: DateTime.now(), + firstDate: first, + lastDate: last, builder: (context, child) => Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( @@ -53,14 +57,22 @@ class _DateRangePickerWidgetState extends State if (picked != null) { if (isStartDate) { widget.startDate.value = picked; + // Auto-adjust endDate if needed + if (widget.endDate.value != null && + widget.endDate.value!.isBefore(picked)) { + widget.endDate.value = picked; + } } else { widget.endDate.value = picked; + // Auto-adjust startDate if needed + if (widget.startDate.value != null && + widget.startDate.value!.isAfter(picked)) { + widget.startDate.value = picked; + } } - if (widget.onDateRangeSelected != null) { - widget.onDateRangeSelected!( - widget.startDate.value, widget.endDate.value); - } + widget.onDateRangeSelected + ?.call(widget.startDate.value, widget.endDate.value); } } diff --git a/lib/helpers/widgets/my_snackbar.dart b/lib/helpers/widgets/my_snackbar.dart index 0b51911..54a6bc8 100644 --- a/lib/helpers/widgets/my_snackbar.dart +++ b/lib/helpers/widgets/my_snackbar.dart @@ -35,7 +35,7 @@ void showAppSnackbar({ message, backgroundColor: backgroundColor, colorText: Colors.white, - snackPosition: SnackPosition.BOTTOM, + snackPosition: SnackPosition.TOP, margin: const EdgeInsets.all(16), borderRadius: 8, duration: const Duration(seconds: 5), diff --git a/lib/model/employees/employee_model.dart b/lib/model/employees/employee_model.dart index 63669cc..d774f23 100644 --- a/lib/model/employees/employee_model.dart +++ b/lib/model/employees/employee_model.dart @@ -71,4 +71,14 @@ class EmployeeModel { 'phoneNumber': phoneNumber.isEmpty ? '-' : phoneNumber, }; } + + /// ✅ Add equality based on unique `id` + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is EmployeeModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; } diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart new file mode 100644 index 0000000..34b62bc --- /dev/null +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/helpers/services/api_service.dart'; + +class EmployeeSelectionBottomSheet extends StatefulWidget { + final List initiallySelected; + final bool multipleSelection; + final String title; + + const EmployeeSelectionBottomSheet({ + Key? key, + this.initiallySelected = const [], + this.multipleSelection = true, + this.title = 'Select Employees', + }) : super(key: key); + + @override + State createState() => + _EmployeeSelectionBottomSheetState(); +} + +class _EmployeeSelectionBottomSheetState + extends State { + final TextEditingController _searchController = TextEditingController(); + final RxBool _isSearching = false.obs; + final RxList _searchResults = [].obs; + late RxList _selectedEmployees; + + @override + void initState() { + super.initState(); + _selectedEmployees = RxList.from(widget.initiallySelected); + _searchEmployees(''); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _searchEmployees(String query) async { + _isSearching.value = true; + final data = await ApiService.searchEmployeesBasic(searchString: query); + final results = (data as List) + .map((e) => EmployeeModel.fromJson(e as Map)) + .toList(); + _searchResults.assignAll(results); + _isSearching.value = false; + } + + void _toggleEmployee(EmployeeModel emp) { + if (widget.multipleSelection) { + if (_selectedEmployees.contains(emp)) { + _selectedEmployees.remove(emp); + } else { + _selectedEmployees.add(emp); + } + _selectedEmployees.refresh(); + } else { + _selectedEmployees.assignAll([emp]); + _selectedEmployees.refresh(); + } + } + + void _handleSubmit() { + if (widget.multipleSelection) { + Navigator.of(context).pop(_selectedEmployees.toList()); + } else { + Navigator.of(context) + .pop(_selectedEmployees.isNotEmpty ? _selectedEmployees.first : null); + } + } + + Widget _searchBar() => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextField( + controller: _searchController, + onChanged: _searchEmployees, + decoration: InputDecoration( + hintText: 'Search employees...', + filled: true, + fillColor: Colors.grey.shade100, + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: () { + _searchController.clear(); + _searchEmployees(''); + }, + ) + : null, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + ), + ); + + Widget _employeeList() => Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: _searchResults.length, + itemBuilder: (context, index) { + final emp = _searchResults[index]; + return Obx(() { + // wrap each tile + final isSelected = _selectedEmployees.contains(emp); + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') + .toUpperCase(), + style: const TextStyle(color: Colors.white), + ), + ), + title: Text('${emp.firstName} ${emp.lastName}'), + subtitle: Text(emp.email), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, + ), + ), + onTap: () => _toggleEmployee(emp), + contentPadding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); + }); + }, + ), + ); + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: widget.title, + onCancel: () => Navigator.of(context).pop(), + onSubmit: _handleSubmit, + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.7, + child: Column(children: [ + _searchBar(), + _employeeList(), + ]), + ), + ); + } +} diff --git a/lib/model/service_project/add_service_project_job_bottom_sheet.dart b/lib/model/service_project/add_service_project_job_bottom_sheet.dart new file mode 100644 index 0000000..64fbbfd --- /dev/null +++ b/lib/model/service_project/add_service_project_job_bottom_sheet.dart @@ -0,0 +1,241 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; +import 'package:marco/controller/service_project/add_service_project_job_controller.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + +class AddServiceProjectJobBottomSheet extends StatefulWidget { + final String projectId; + const AddServiceProjectJobBottomSheet({super.key, required this.projectId}); + + @override + State createState() => + _AddServiceProjectJobBottomSheetState(); +} + +class _AddServiceProjectJobBottomSheetState + extends State with UIMixin { + final formKey = GlobalKey(); + final controller = Get.put(AddServiceProjectJobController()); + + final TextEditingController _searchController = TextEditingController(); + late RxList _selectedEmployees; + + @override + void initState() { + super.initState(); + _selectedEmployees = + RxList.from(controller.selectedAssignees); + } + + @override + void dispose() { + _searchController.dispose(); + Get.delete(); + super.dispose(); + } + + InputDecoration _inputDecoration(String hint) => InputDecoration( + hintText: hint, + hintStyle: MyTextStyle.bodySmall(xMuted: true), + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + isDense: true, + ); + + Widget _labelWithStar(String label, {bool required = false}) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + MyText.labelMedium(label), + if (required) + const Text(" *", style: TextStyle(color: Colors.red, fontSize: 14)), + ], + ); + + Widget _textField(String label, TextEditingController ctrl, + {bool required = false, int maxLines = 1}) => + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _labelWithStar(label, required: required), + MySpacing.height(8), + TextFormField( + controller: ctrl, + maxLines: maxLines, + decoration: _inputDecoration("Enter $label"), + validator: required + ? (v) => (v == null || v.trim().isEmpty) + ? "$label is required" + : null + : null, + ), + ], + ); + Widget _employeeSelector() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _labelWithStar("Select Assignees", required: true), + MySpacing.height(8), + GestureDetector( + onTap: () async { + final selectedEmployees = + await showModalBottomSheet>( + context: context, + isScrollControlled: true, + builder: (_) => EmployeeSelectionBottomSheet( + multipleSelection: true, + initiallySelected: _selectedEmployees, + ), + ); + + if (selectedEmployees != null) { + setState(() { + _selectedEmployees.assignAll(selectedEmployees); + }); + } + }, + child: AbsorbPointer( + child: TextFormField( + decoration: _inputDecoration("Select Employees"), + controller: TextEditingController( + text: _selectedEmployees.isEmpty + ? "" + : _selectedEmployees + .map((e) => "${e.firstName} ${e.lastName}") + .join(", "), + ), + validator: (v) => _selectedEmployees.isEmpty + ? "Please select employees" + : null, + ), + ), + ), + ], + ); + + Widget _tagInput() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 48, + child: TextField( + controller: controller.tagCtrl, + onSubmitted: (v) { + final value = v.trim(); + if (value.isNotEmpty && + !controller.enteredTags.contains(value)) { + controller.enteredTags.add(value); + } + controller.tagCtrl.clear(); + }, + decoration: _inputDecoration("Start typing to add tags"), + ), + ), + MySpacing.height(8), + Obx(() => Wrap( + spacing: 8, + children: controller.enteredTags + .map((tag) => Chip( + label: Text(tag), + onDeleted: () => controller.enteredTags.remove(tag))) + .toList(), + )), + ], + ); + + + + void _toggleEmployee(EmployeeModel emp) { + final contains = _selectedEmployees.contains(emp); + if (contains) { + _selectedEmployees.remove(emp); + } else { + _selectedEmployees.add(emp); + } + controller.toggleAssignee(emp); + } + + void _handleSubmit() { + if (!(formKey.currentState?.validate() ?? false)) return; + controller.titleCtrl.text = controller.titleCtrl.text.trim(); + controller.descCtrl.text = controller.descCtrl.text.trim(); + controller.createJob(widget.projectId); + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: "Add New Job", + onCancel: () => Get.back(), + onSubmit: _handleSubmit, + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _textField("Title", controller.titleCtrl, required: true), + MySpacing.height(16), + Obx(() { + if (_searchController.text.isNotEmpty) + return const SizedBox.shrink(); + return Wrap( + spacing: 8, + runSpacing: 4, + children: _selectedEmployees + .map( + (emp) => Chip( + label: Text('${emp.firstName} ${emp.lastName}'), + onDeleted: () => _toggleEmployee(emp), + ), + ) + .toList(), + ); + }), + _employeeSelector(), + MySpacing.height(16), + MyText.labelMedium("Tags (Optional)"), + MySpacing.height(8), + _tagInput(), + MySpacing.height(16), + _labelWithStar("Select Date Range", required: true), + MySpacing.height(8), + DateRangePickerWidget( + startDate: controller.startDate, + endDate: controller.dueDate, + startLabel: "Start Date", + endLabel: "Due Date", + onDateRangeSelected: (start, end) { + controller.startDate.value = start ?? DateTime.now(); + controller.dueDate.value = + end ?? DateTime.now().add(const Duration(days: 1)); + }, + ), + MySpacing.height(16), + _textField("Description", controller.descCtrl, + required: true, maxLines: 3), + ], + ), + ), + ); + } +} diff --git a/lib/model/service_project/job_list_model.dart b/lib/model/service_project/job_list_model.dart new file mode 100644 index 0000000..2ddd1ce --- /dev/null +++ b/lib/model/service_project/job_list_model.dart @@ -0,0 +1,237 @@ +class JobResponse { + final bool success; + final String message; + final int statusCode; + final String timestamp; + final JobData? data; + final dynamic errors; + + JobResponse({ + required this.success, + required this.message, + required this.statusCode, + required this.timestamp, + this.data, + this.errors, + }); + + factory JobResponse.fromJson(Map json) { + return JobResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + statusCode: json['statusCode'] ?? 0, + timestamp: json['timestamp'] ?? '', + data: json['data'] != null ? JobData.fromJson(json['data']) : null, + errors: json['errors'], + ); + } +} + +class JobData { + final int currentPage; + final int totalPages; + final int totalEntities; + final List? data; + + JobData({ + required this.currentPage, + required this.totalPages, + required this.totalEntities, + this.data, + }); + + factory JobData.fromJson(Map json) { + return JobData( + currentPage: json['currentPage'] ?? 0, + totalPages: json['totalPages'] ?? 0, + totalEntities: json['totalEntities'] ?? 0, + data: (json['data'] as List?) + ?.map((e) => JobEntity.fromJson(e)) + .toList(), + ); + } +} + +class JobEntity { + final String id; + final String title; + final String description; + final Project project; + final List? assignees; + final Status status; + final String startDate; + final String dueDate; + final bool isActive; + final String createdAt; + final CreatedBy createdBy; + final List? tags; + + JobEntity({ + required this.id, + required this.title, + required this.description, + required this.project, + this.assignees, + required this.status, + required this.startDate, + required this.dueDate, + required this.isActive, + required this.createdAt, + required this.createdBy, + this.tags, + }); + + factory JobEntity.fromJson(Map json) { + return JobEntity( + id: json['id'] ?? '', + title: json['title'] ?? '', + description: json['description'] ?? '', + project: Project.fromJson(json['project']), + assignees: (json['assignees'] as List?) + ?.map((e) => Assignee.fromJson(e)) + .toList(), + status: Status.fromJson(json['status']), + startDate: json['startDate'] ?? '', + dueDate: json['dueDate'] ?? '', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? '', + createdBy: CreatedBy.fromJson(json['createdBy']), + tags: (json['tags'] as List?) + ?.map((e) => Tag.fromJson(e)) + .toList(), + ); + } +} + +class Project { + final String id; + final String name; + final String shortName; + final String assignedDate; + final String contactName; + final String contactPhone; + final String contactEmail; + + Project({ + required this.id, + required this.name, + required this.shortName, + required this.assignedDate, + required this.contactName, + required this.contactPhone, + required this.contactEmail, + }); + + factory Project.fromJson(Map json) { + return Project( + id: json['id'] ?? '', + name: json['name'] ?? '', + shortName: json['shortName'] ?? '', + assignedDate: json['assignedDate'] ?? '', + contactName: json['contactName'] ?? '', + contactPhone: json['contactPhone'] ?? '', + contactEmail: json['contactEmail'] ?? '', + ); + } +} + +class Assignee { + final String id; + final String firstName; + final String lastName; + final String email; + final String photo; + final String jobRoleId; + final String jobRoleName; + + Assignee({ + required this.id, + required this.firstName, + required this.lastName, + required this.email, + required this.photo, + required this.jobRoleId, + required this.jobRoleName, + }); + + factory Assignee.fromJson(Map json) { + return Assignee( + id: json['id'] ?? '', + firstName: json['firstName'] ?? '', + lastName: json['lastName'] ?? '', + email: json['email'] ?? '', + photo: json['photo'] ?? '', + jobRoleId: json['jobRoleId'] ?? '', + jobRoleName: json['jobRoleName'] ?? '', + ); + } +} + +class Status { + final String id; + final String name; + final String displayName; + + Status({ + required this.id, + required this.name, + required this.displayName, + }); + + factory Status.fromJson(Map json) { + return Status( + id: json['id'] ?? '', + name: json['name'] ?? '', + displayName: json['displayName'] ?? '', + ); + } +} + +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'] ?? '', + ); + } +} + +class Tag { + final String id; + final String name; + + Tag({ + required this.id, + required this.name, + }); + + factory Tag.fromJson(Map json) { + return Tag( + id: json['id'] ?? '', + name: json['name'] ?? '', + ); + } +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 6ff69e3..9cbe20d 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -106,6 +106,7 @@ class _DashboardScreenState extends State with UIMixin { MenuItems.directory, MenuItems.finance, MenuItems.documents, + MenuItems.serviceProjects ]; final Map cardMeta = { @@ -123,6 +124,8 @@ class _DashboardScreenState extends State with UIMixin { _DashboardCardMeta(LucideIcons.wallet, contentTheme.info), MenuItems.documents: _DashboardCardMeta(LucideIcons.file_text, contentTheme.info), + MenuItems.serviceProjects: + _DashboardCardMeta(LucideIcons.package, contentTheme.info), }; // Filter only available menus that exist in cardMeta diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart index e9f18b9..ad39ef7 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -5,7 +5,10 @@ import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/service_project/service_project_details_screen_controller.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/model/service_project/add_service_project_job_bottom_sheet.dart'; +import 'package:marco/helpers/utils/date_time_utils.dart'; class ServiceProjectDetailsScreen extends StatefulWidget { final String projectId; @@ -19,9 +22,10 @@ class ServiceProjectDetailsScreen extends StatefulWidget { class _ServiceProjectDetailsScreenState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, UIMixin { late final TabController _tabController; late final ServiceProjectDetailsController controller; + final ScrollController _jobScrollController = ScrollController(); @override void initState() { @@ -30,15 +34,32 @@ class _ServiceProjectDetailsScreenState _tabController = TabController(length: 2, vsync: this); controller = Get.put(ServiceProjectDetailsController()); - // Fetch project detail safely after first frame WidgetsBinding.instance.addPostFrameCallback((_) { controller.setProjectId(widget.projectId); }); + + _tabController.addListener(() { + if (!_tabController.indexIsChanging) { + setState(() {}); // rebuild to show/hide FAB + if (_tabController.index == 1 && controller.jobList.isEmpty) { + controller.fetchProjectJobs(); + } + } + }); + + _jobScrollController.addListener(() { + if (_tabController.index == 1 && + _jobScrollController.position.pixels >= + _jobScrollController.position.maxScrollExtent - 100) { + controller.fetchMoreJobs(); + } + }); } @override void dispose() { _tabController.dispose(); + _jobScrollController.dispose(); super.dispose(); } @@ -157,7 +178,7 @@ class _ServiceProjectDetailsScreenState child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Header + // Header Card Card( elevation: 2, shadowColor: Colors.black12, @@ -292,6 +313,155 @@ class _ServiceProjectDetailsScreenState ); } + Widget _buildJobsTab() { + return Obx(() { + if (controller.isJobLoading.value && controller.jobList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.jobErrorMessage.value.isNotEmpty && + controller.jobList.isEmpty) { + return Center( + child: MyText.bodyMedium(controller.jobErrorMessage.value)); + } + + if (controller.jobList.isEmpty) { + return Center(child: MyText.bodyMedium("No jobs found")); + } + + return ListView.separated( + controller: _jobScrollController, + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: controller.jobList.length + 1, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + if (index == controller.jobList.length) { + return controller.hasMoreJobs.value + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center(child: CircularProgressIndicator()), + ) + : const SizedBox.shrink(); + } + + final job = controller.jobList[index]; + return Card( + elevation: 3, + shadowColor: Colors.black26, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Job Title + MyText.titleMedium(job.title, fontWeight: 700), + MySpacing.height(6), + + // Job Description + MyText.bodySmall( + job.description.isNotEmpty + ? job.description + : "No description provided", + color: Colors.grey[700], + ), + + // Tags + if (job.tags != null && job.tags!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Wrap( + spacing: 2, + runSpacing: 4, + children: job.tags!.map((tag) { + return Chip( + label: Text( + tag.name, + style: const TextStyle(fontSize: 12), + ), + backgroundColor: Colors.grey[200], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ); + }).toList(), + ), + ), + + MySpacing.height(8), + + // Assignees & Status + Row( + children: [ + if (job.assignees != null && job.assignees!.isNotEmpty) + ...job.assignees!.map((assignee) { + return Padding( + padding: const EdgeInsets.only(right: 6), + child: CircleAvatar( + radius: 12, + backgroundImage: assignee.photo.isNotEmpty + ? NetworkImage(assignee.photo) + : null, + child: assignee.photo.isEmpty + ? Text(assignee.firstName[0]) + : null, + ), + ); + }).toList(), + ], + ), + + MySpacing.height(8), + + // Date Row with Status Chip + Row( + children: [ + // Dates (same as existing) + const Icon(Icons.calendar_today_outlined, + size: 14, color: Colors.grey), + MySpacing.width(4), + Text( + "${DateTimeUtils.convertUtcToLocal(job.startDate, format: 'dd MMM yyyy')} to " + "${DateTimeUtils.convertUtcToLocal(job.dueDate, format: 'dd MMM yyyy')}", + style: + const TextStyle(fontSize: 12, color: Colors.grey), + ), + + const Spacer(), + + // Status Chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: job.status.name.toLowerCase() == 'completed' + ? Colors.green[100] + : Colors.orange[100], + borderRadius: BorderRadius.circular(5), + ), + child: Text( + job.status.displayName, + style: TextStyle( + fontSize: 12, + color: job.status.name.toLowerCase() == 'completed' + ? Colors.green[800] + : Colors.orange[800], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -320,7 +490,7 @@ class _ServiceProjectDetailsScreenState mainAxisSize: MainAxisSize.min, children: [ MyText.titleLarge( - 'Service Projects', + 'Service Project Details', fontWeight: 700, color: Colors.black, ), @@ -355,50 +525,69 @@ class _ServiceProjectDetailsScreenState ), ), ), - body: Column( - children: [ - // ---------------- TabBar ---------------- - Container( - color: Colors.white, - child: TabBar( - controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.red, - indicatorWeight: 3, - isScrollable: false, - tabs: [ - Tab(child: MyText.bodyMedium("Profile")), - Tab(child: MyText.bodyMedium("Jobs")), - ], - ), - ), - - // ---------------- TabBarView ---------------- - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - if (controller.errorMessage.value.isNotEmpty) { - return Center( - child: MyText.bodyMedium(controller.errorMessage.value)); - } - - return TabBarView( + body: SafeArea( + child: Column( + children: [ + // TabBar + Container( + color: Colors.white, + child: TabBar( controller: _tabController, - children: [ - // Profile Tab - _buildProfileTab(), - - // Jobs Tab - empty - Container(color: Colors.white), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + indicatorColor: Colors.red, + indicatorWeight: 3, + isScrollable: false, + tabs: [ + Tab(child: MyText.bodyMedium("Profile")), + Tab(child: MyText.bodyMedium("Jobs")), ], - ); - }), - ), - ], + ), + ), + + // TabBarView + Expanded( + child: Obx(() { + if (controller.isLoading.value && + controller.projectDetail.value == null) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.errorMessage.value.isNotEmpty && + controller.projectDetail.value == null) { + return Center( + child: MyText.bodyMedium(controller.errorMessage.value)); + } + + return TabBarView( + controller: _tabController, + children: [ + _buildProfileTab(), + _buildJobsTab(), + ], + ); + }), + ), + ], + ), ), + floatingActionButton: _tabController.index == 1 + ? FloatingActionButton.extended( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AddServiceProjectJobBottomSheet( + projectId: widget.projectId, + ), + ); + }, + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.add), + label: MyText.bodyMedium("Add Job", color: Colors.white), + ) + : null, + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } } diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 47311c4..8831369 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -8,6 +8,7 @@ import 'package:marco/controller/service_project/service_project_screen_controll import 'package:marco/model/service_project/service_projects_list_model.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/view/service_project/service_project_details_screen.dart'; +import 'package:marco/controller/project_controller.dart'; class ServiceProjectScreen extends StatefulWidget { const ServiceProjectScreen({super.key}); @@ -211,10 +212,41 @@ class _ServiceProjectScreenState extends State onPressed: () => Get.toNamed('/dashboard'), ), MySpacing.width(8), - MyText.titleLarge( - 'Service Projects', - fontWeight: 700, - color: Colors.black, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Service Projects', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + GetBuilder( + builder: (projectController) { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), ), ], ), From bdfc492e6596d6a9636715050eb34a01614b8d08 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 14 Nov 2025 15:36:32 +0530 Subject: [PATCH 06/60] removed unused code --- .../add_service_project_job_controller.dart | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/lib/controller/service_project/add_service_project_job_controller.dart b/lib/controller/service_project/add_service_project_job_controller.dart index 6195dab..caa9a6f 100644 --- a/lib/controller/service_project/add_service_project_job_controller.dart +++ b/lib/controller/service_project/add_service_project_job_controller.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; class AddServiceProjectJobController extends GetxController { @@ -31,7 +30,6 @@ class AddServiceProjectJobController extends GetxController { @override void onInit() { super.onInit(); - searchEmployees(""); // pass empty string safely } @override @@ -42,31 +40,6 @@ class AddServiceProjectJobController extends GetxController { super.onClose(); } - Future searchEmployees(String query) async { - if (query.trim().isEmpty) { - employeeSearchResults.clear(); - return; - } - isSearchingEmployees.value = true; - try { - final data = - await ApiService.searchEmployeesBasic(searchString: query.trim()); - if (data is List) { - employeeSearchResults.assignAll( - data - .map((e) => EmployeeModel.fromJson(e as Map)) - .toList(), - ); - } else { - employeeSearchResults.clear(); - } - } catch (e) { - logSafe("Error searching employees: $e", level: LogLevel.error); - employeeSearchResults.clear(); - } finally { - isSearchingEmployees.value = false; - } - } /// Toggle employee selection void toggleAssignee(EmployeeModel employee) { From c631f8b0926a0bd513f7a52aff22e9f5b71edff3 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 07/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 6 + lib/helpers/services/api_service.dart | 54 ++ .../employees/employee_details_model.dart | 4 +- .../employees/employee_detail_screen.dart | 60 ++ lib/view/employees/employees_screen.dart | 167 ++++-- .../manage_reporting_bottom_sheet.dart | 557 ++++++++++++++++++ lib/view/finance/advance_payment_screen.dart | 5 - 7 files changed, 809 insertions(+), 44 deletions(-) create mode 100644 lib/view/employees/manage_reporting_bottom_sheet.dart diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 35e7831..100c1dd 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -130,6 +130,12 @@ class ApiEndpoints { static const String getAssignedServices = "/Project/get/assigned/services"; static const String getAdvancePayments = '/Expense/get/transactions'; + // Organization Hierarchy endpoints + static const String getOrganizationHierarchyList = + "/organization/hierarchy/list"; + static const String manageOrganizationHierarchy = + "/organization/hierarchy/manage"; + // Service Project Module API Endpoints static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index a5e2219..f2e8fbc 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -850,6 +850,60 @@ class ApiService { } } + /// Fetch hierarchy list for an employee + static Future?> getOrganizationHierarchyList( + String employeeId) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + final endpoint = "${ApiEndpoints.getOrganizationHierarchyList}/$employeeId"; + + return _getRequest(endpoint).then( + (res) => res != null + ? _parseResponse(res, label: 'Organization Hierarchy List') + : null, + ); + } + + /// Manage (create/update) organization hierarchy (assign reporters) for an employee + /// payload is a List> with objects like: + /// { "reportToId": "", "isPrimary": true, "isActive": true } + static Future manageOrganizationHierarchy({ + required String employeeId, + required List> payload, + }) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + + final endpoint = "${ApiEndpoints.manageOrganizationHierarchy}/$employeeId"; + + logSafe("manageOrganizationHierarchy for $employeeId payload: $payload"); + + try { + final response = await _postRequest(endpoint, payload); + if (response == null) { + logSafe("Manage hierarchy failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Manage hierarchy response status: ${response.statusCode}"); + logSafe("Manage hierarchy response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Manage hierarchy succeeded"); + return true; + } + + logSafe("Manage hierarchy failed: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.error); + return false; + } catch (e, stack) { + logSafe("Exception while manageOrganizationHierarchy: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + /// Get Master Currencies static Future getMasterCurrenciesApi() async { const endpoint = ApiEndpoints.getMasterCurrencies; diff --git a/lib/model/employees/employee_details_model.dart b/lib/model/employees/employee_details_model.dart index b5c22f4..e999836 100644 --- a/lib/model/employees/employee_details_model.dart +++ b/lib/model/employees/employee_details_model.dart @@ -22,7 +22,9 @@ class EmployeeDetailsModel { final bool hasApplicationAccess; final String? organizationId; final String? aadharNumber; - final String? panNumber; + final String? panNumber; + + EmployeeDetailsModel({ required this.id, required this.firstName, diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 81b32de..7450384 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -11,6 +11,8 @@ import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { @@ -269,6 +271,64 @@ class _EmployeeDetailPageState extends State with UIMixin { ), MySpacing.height(16), + _buildSectionCard( + title: 'Manage Reporting', + titleIcon: Icons.people_outline, + children: [ + GestureDetector( + onTap: () async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ManageReportingBottomSheet( + initialEmployee: EmployeeModel( + id: employee.id, + employeeId: employee.id.toString(), + firstName: employee.firstName ?? "", + lastName: employee.lastName ?? "", + name: + "${employee.firstName} ${employee.lastName}", + email: employee.email ?? "", + jobRole: employee.jobRole ?? "", + jobRoleID: "0", + designation: employee.jobRole ?? "", + phoneNumber: employee.phoneNumber ?? "", + activity: 0, + action: 0, + ), + hideMainSelector: true, + hideLoggedUserFromSelection: + true, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + const Icon(Icons.manage_accounts_outlined, + color: Colors.grey), + const SizedBox(width: 16), + const Expanded( + child: Text( + 'View / Update Reporting Managers', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ), + const Icon(Icons.arrow_forward_ios_rounded, + size: 16, color: Colors.grey), + ], + ), + ), + ), + ], + ), + // Contact Information Section _buildSectionCard( title: 'Contact Information', diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 5a6d925..db1f821 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -16,6 +16,7 @@ import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/view/employees/employee_profile_screen.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -64,13 +65,17 @@ class _EmployeesScreenState extends State with UIMixin { final searchQuery = query.toLowerCase(); final filtered = query.isEmpty ? List.from(employees) - : employees.where((e) => - e.name.toLowerCase().contains(searchQuery) || - e.email.toLowerCase().contains(searchQuery) || - e.phoneNumber.toLowerCase().contains(searchQuery) || - e.jobRole.toLowerCase().contains(searchQuery), - ).toList(); - filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + : employees + .where( + (e) => + e.name.toLowerCase().contains(searchQuery) || + e.email.toLowerCase().contains(searchQuery) || + e.phoneNumber.toLowerCase().contains(searchQuery) || + e.jobRole.toLowerCase().contains(searchQuery), + ) + .toList(); + filtered + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); _filteredEmployees.assignAll(filtered); } @@ -106,7 +111,6 @@ class _EmployeesScreenState extends State with UIMixin { await _refreshEmployees(); } - @override Widget build(BuildContext context) { return Scaffold( @@ -160,7 +164,8 @@ class _EmployeesScreenState extends State with UIMixin { child: Row( children: [ IconButton( - icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), onPressed: () => Get.offNamed('/dashboard'), ), MySpacing.width(8), @@ -206,7 +211,8 @@ class _EmployeesScreenState extends State with UIMixin { Widget _buildFloatingActionButton() { return Obx(() { if (_permissionController.isLoading.value) return const SizedBox.shrink(); - final hasPermission = _permissionController.hasPermission(Permissions.manageEmployees); + final hasPermission = + _permissionController.hasPermission(Permissions.manageEmployees); if (!hasPermission) return const SizedBox.shrink(); return InkWell( @@ -218,7 +224,8 @@ class _EmployeesScreenState extends State with UIMixin { color: contentTheme.primary, borderRadius: BorderRadius.circular(28), boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3)), + BoxShadow( + color: Colors.black26, blurRadius: 6, offset: Offset(0, 3)), ], ), child: const Row( @@ -235,33 +242,116 @@ class _EmployeesScreenState extends State with UIMixin { } Widget _buildSearchField() { - return SizedBox( - height: 36, - child: TextField( - controller: _searchController, - style: const TextStyle(fontSize: 13, height: 1.2), - decoration: InputDecoration( - isDense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - hintText: 'Search employees...', - filled: true, - fillColor: Colors.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300, width: 1), + return Padding( + padding: MySpacing.xy(8, 8), + child: Row( + children: [ + // Search field + Expanded( + child: SizedBox( + height: 35, + child: TextField( + controller: _searchController, + style: const TextStyle(fontSize: 13, height: 1.2), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: + const Icon(Icons.search, size: 20, color: Colors.grey), + suffixIcon: ValueListenableBuilder( + valueListenable: _searchController, + builder: (context, value, _) { + if (value.text.isEmpty) return const SizedBox.shrink(); + return IconButton( + icon: const Icon(Icons.clear, + size: 20, color: Colors.grey), + onPressed: () { + _searchController.clear(); + _filterEmployees(''); + }, + ); + }, + ), + hintText: 'Search employees...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + onChanged: (_) => _filterEmployees(_searchController.text), + ), + ), ), - suffixIcon: _searchController.text.isNotEmpty - ? GestureDetector( - onTap: () { - _searchController.clear(); - _filterEmployees(''); - }, - child: const Icon(Icons.close, size: 18, color: Colors.grey), - ) - : null, - ), - onChanged: (_) => _filterEmployees(_searchController.text), + MySpacing.width(10), + + // Three dots menu (Manage Reporting) + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: + const Icon(Icons.more_vert, size: 20, color: Colors.black87), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + itemBuilder: (context) { + List> menuItems = []; + + // Section: Actions + menuItems.add( + const PopupMenuItem( + enabled: false, + height: 30, + child: Text( + "Actions", + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ); + + // Manage Reporting option + menuItems.add( + PopupMenuItem( + value: 1, + child: Row( + children: [ + const Icon(Icons.manage_accounts_outlined, + size: 20, color: Colors.black87), + const SizedBox(width: 10), + const Expanded(child: Text("Manage Reporting")), + Icon(Icons.chevron_right, + size: 20, color: contentTheme.primary), + ], + ), + onTap: () { + Future.delayed(Duration.zero, () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => const ManageReportingBottomSheet(), + ); + }); + }, + ), + ); + + return menuItems; + }, + ), + ), + ], ), ); } @@ -283,7 +373,8 @@ class _EmployeesScreenState extends State with UIMixin { return Padding( padding: const EdgeInsets.only(top: 60), child: Center( - child: MyText.bodySmall("No Employees Found", fontWeight: 600, color: Colors.grey[700]), + child: MyText.bodySmall("No Employees Found", + fontWeight: 600, color: Colors.grey[700]), ), ); } diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart new file mode 100644 index 0000000..3eb5dd4 --- /dev/null +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -0,0 +1,557 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class ManageReportingBottomSheet extends StatefulWidget { + final EmployeeModel? initialEmployee; + final bool hideMainSelector; + final bool renderAsCard; + final bool hideLoggedUserFromSelection; // ✅ new + + const ManageReportingBottomSheet({ + super.key, + this.initialEmployee, + this.hideMainSelector = false, + this.renderAsCard = false, + this.hideLoggedUserFromSelection = false, // default false + }); + + @override + State createState() => + _ManageReportingBottomSheetState(); +} + +class _ManageReportingBottomSheetState + extends State { + final EmployeesScreenController _employeeController = Get.find(); + final TextEditingController _primaryController = TextEditingController(); + final TextEditingController _secondaryController = TextEditingController(); + + final RxList _filteredPrimary = [].obs; + final RxList _filteredSecondary = [].obs; + final RxList _selectedPrimary = [].obs; + final RxList _selectedSecondary = [].obs; + + final TextEditingController _selectEmployeeController = + TextEditingController(); + final RxList _filteredEmployees = [].obs; + EmployeeModel? _selectedEmployee; + + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + _primaryController + .addListener(() => _filterEmployees(_primaryController.text, true)); + _secondaryController + .addListener(() => _filterEmployees(_secondaryController.text, false)); + _selectEmployeeController + .addListener(() => _filterMainEmployee(_selectEmployeeController.text)); + + // if the parent passed an initialEmployee (profile page), preselect & load hierarchy + if (widget.initialEmployee != null) { + // delay to let widget finish first build + WidgetsBinding.instance.addPostFrameCallback((_) { + _onMainEmployeeSelected(widget.initialEmployee!); + }); + } + } + + @override + void dispose() { + _primaryController.dispose(); + _secondaryController.dispose(); + _selectEmployeeController.dispose(); + super.dispose(); + } + + void _filterMainEmployee(String query) { + final employees = _employeeController.employees; + final searchQuery = query.toLowerCase(); + + final filtered = query.isEmpty + ? [] + : employees + .where((e) => e.name.toLowerCase().contains(searchQuery)) + .take(6) + .toList(); + + _filteredEmployees.assignAll(filtered); + } + + void _filterEmployees(String query, bool isPrimary) { + final employees = _employeeController.employees; + final searchQuery = query.toLowerCase(); + + final filtered = query.isEmpty + ? [] + : employees + .where((e) => e.name.toLowerCase().contains(searchQuery)) + .take(6) + .toList(); + + if (isPrimary) { + _filteredPrimary.assignAll(filtered); + } else { + _filteredSecondary.assignAll(filtered); + } + } + + void _toggleSelection(EmployeeModel emp, bool isPrimary) { + final list = isPrimary ? _selectedPrimary : _selectedSecondary; + + if (isPrimary) { + //Allow only one primary employee at a time + list.clear(); + list.add(emp); + } else { + // ✅ Secondary employees can still have multiple selections + if (list.any((e) => e.id == emp.id)) { + list.removeWhere((e) => e.id == emp.id); + } else { + list.add(emp); + } + } + } + + /// helper to find employee by id from controller list (returns nullable) + EmployeeModel? _findEmployeeById(String id) { + for (final e in _employeeController.employees) { + if (e.id == id) return e; + } + return null; + } + + /// Called when user taps an employee from dropdown to manage reporting for. + /// It sets selected employee and fetches existing hierarchy to preselect reporters. + Future _onMainEmployeeSelected(EmployeeModel emp) async { + setState(() { + _selectedEmployee = emp; + _selectEmployeeController.text = emp.name; + _filteredEmployees.clear(); + }); + + // Clear previous selections + _selectedPrimary.clear(); + _selectedSecondary.clear(); + + // Fetch existing reporting hierarchy for this employee + try { + final data = await ApiService.getOrganizationHierarchyList(emp.id); + if (data == null || data.isEmpty) return; + + for (final item in data) { + try { + final isPrimary = item['isPrimary'] == true; + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + + final match = _findEmployeeById(reportToId); + if (match == null) continue; + + // ✅ Skip the employee whose profile is open + if (widget.initialEmployee != null && + match.id == widget.initialEmployee!.id) { + continue; + } + + if (isPrimary) { + if (!_selectedPrimary.any((e) => e.id == match.id)) { + _selectedPrimary.add(match); + } + } else { + if (!_selectedSecondary.any((e) => e.id == match.id)) { + _selectedSecondary.add(match); + } + } + } catch (_) { + // ignore malformed items + } + } + } catch (e) { + // Fetch failure - show a subtle snackbar + showAppSnackbar( + title: 'Error', + message: 'Failed to load existing reporting.', + type: SnackbarType.error); + } + } + + void _resetForm() { + setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _selectedPrimary.clear(); + _selectedSecondary.clear(); + _filteredEmployees.clear(); + _filteredPrimary.clear(); + _filteredSecondary.clear(); + }); + } + + void _resetReportersOnly() { + _selectedPrimary.clear(); + _selectedSecondary.clear(); + _primaryController.clear(); + _secondaryController.clear(); + _filteredPrimary.clear(); + _filteredSecondary.clear(); + } + + Future _handleSubmit() async { + if (_selectedEmployee == null) { + showAppSnackbar( + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); + return; + } + if (_selectedPrimary.isEmpty) { + showAppSnackbar( + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); + return; + } + + final List> payload = []; + + for (final emp in _selectedPrimary) { + payload.add({ + "reportToId": emp.id, + "isPrimary": true, + "isActive": true, + }); + } + for (final emp in _selectedSecondary) { + payload.add({ + "reportToId": emp.id, + "isPrimary": false, + "isActive": true, + }); + } + + setState(() => _isSubmitting = true); + // show loader + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + + final employeeId = _selectedEmployee!.id; + final success = await ApiService.manageOrganizationHierarchy( + employeeId: employeeId, + payload: payload, + ); + + // hide loader + if (Get.isDialogOpen == true) Get.back(); + + setState(() => _isSubmitting = false); + + if (success) { + showAppSnackbar( + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); + + // Optionally refresh the saved hierarchy (not necessary here) but we can call: + await ApiService.getOrganizationHierarchyList(employeeId); + + // Keep sheet open and reset reporter selections for next assignment + _resetForm(); + } else { + showAppSnackbar( + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); + } + } + + void _handleCancel() => Navigator.pop(context); + + @override + Widget build(BuildContext context) { + // build the same child column content you already had, but assign to a variable + final Widget content = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // conditional: show search section or simple header + if (widget.hideMainSelector) + _buildMainEmployeeHeader() + else + _buildMainEmployeeSection(), + + MySpacing.height(20), + + // Primary Employees section + _buildSearchSection( + label: "Primary Reporting Manager*", + controller: _primaryController, + filteredList: _filteredPrimary, + selectedList: _selectedPrimary, + isPrimary: true, + ), + + MySpacing.height(20), + + // Secondary Employees section + _buildSearchSection( + label: "Secondary Reporting Manager", + controller: _secondaryController, + filteredList: _filteredSecondary, + selectedList: _selectedSecondary, + isPrimary: false, + ), + ], + ); + + if (widget.renderAsCard) { + // Inline card for profile screen + return Card( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(12), + child: content, + ), + ); + } + + // default: existing bottom sheet usage + return BaseBottomSheet( + title: "Manage Reporting", + submitText: "Submit", + isSubmitting: _isSubmitting, + onCancel: _handleCancel, + onSubmit: _handleSubmit, + child: content, + ); + } + + Widget _buildMainEmployeeHeader() { + // show selected employee name non-editable (chip-style) + final emp = _selectedEmployee; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium("Selected Employee", fontWeight: 600), + MySpacing.height(8), + if (emp != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Chip( + label: Text(emp.name, style: const TextStyle(fontSize: 12)), + backgroundColor: Colors.indigo.shade50, + labelStyle: TextStyle(color: AppTheme.primaryColor), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), + ), + ) + else + const Text('No employee selected', + style: TextStyle(color: Colors.grey)), + ], + ); + } + + Widget _buildSearchSection({ + required String label, + required TextEditingController controller, + required RxList filteredList, + required RxList selectedList, + required bool isPrimary, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium(label, fontWeight: 600), + MySpacing.height(8), + + // Search field + TextField( + controller: controller, + decoration: InputDecoration( + hintText: "Type to search employees...", + isDense: true, + filled: true, + fillColor: Colors.grey[50], + prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + ), + + // Dropdown suggestions + Obx(() { + if (filteredList.isEmpty) return const SizedBox.shrink(); + return Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: ListView.builder( + itemCount: filteredList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, index) { + final emp = filteredList[index]; + final isSelected = selectedList.any((e) => e.id == emp.id); + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 14, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + child: MyText.labelSmall( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + fontWeight: 600, + color: AppTheme.primaryColor, + ), + ), + title: Text(emp.name, style: const TextStyle(fontSize: 13)), + trailing: Icon( + isSelected + ? Icons.check_circle + : Icons.radio_button_unchecked, + color: isSelected ? AppTheme.primaryColor : Colors.grey, + size: 18, + ), + onTap: () { + _toggleSelection(emp, isPrimary); + filteredList.clear(); + controller.clear(); + }, + ); + }, + ), + ); + }), + + MySpacing.height(10), + + // Selected employees as chips + Obx(() { + if (selectedList.isEmpty) return const SizedBox.shrink(); + return Wrap( + spacing: 6, + runSpacing: 6, + children: selectedList.map((emp) { + return Chip( + label: Text(emp.name, style: const TextStyle(fontSize: 12)), + backgroundColor: Colors.indigo.shade50, + labelStyle: TextStyle(color: AppTheme.primaryColor), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), + ); + }).toList(), + ); + }), + ], + ); + } + + Widget _buildMainEmployeeSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium("Select Employee *", fontWeight: 600), + MySpacing.height(8), + TextField( + controller: _selectEmployeeController, + decoration: InputDecoration( + hintText: "Type to search employee...", + isDense: true, + filled: true, + fillColor: Colors.grey[50], + prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + ), + Obx(() { + if (_filteredEmployees.isEmpty) return const SizedBox.shrink(); + return Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: ListView.builder( + itemCount: _filteredEmployees.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, index) { + final emp = _filteredEmployees[index]; + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 14, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + child: MyText.labelSmall( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + fontWeight: 600, + color: AppTheme.primaryColor, + ), + ), + title: Text(emp.name, style: const TextStyle(fontSize: 13)), + onTap: () => _onMainEmployeeSelected(emp), + ); + }, + ), + ); + }), + if (_selectedEmployee != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Chip( + label: Text(_selectedEmployee!.name, + style: const TextStyle(fontSize: 12)), + backgroundColor: Colors.indigo.shade50, + labelStyle: TextStyle(color: AppTheme.primaryColor), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + // clear selected reporters too, since employee changed + _resetReportersOnly(); + }), + ), + ), + ], + ); + } +} diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 96d14d3..2c1fe40 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -190,11 +190,6 @@ class _AdvancePaymentScreenState extends State ), ), ), - const SizedBox(width: 4), - IconButton( - icon: const Icon(Icons.tune, color: Colors.black), - onPressed: () {}, - ), ], ), ); From 1b883ac52446875cf699cc16139e5094a10a7713 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 08/60] implementation of manage reporting inside employee profile --- .../employee/employees_screen_controller.dart | 50 +++++++ .../employees/employee_detail_screen.dart | 71 +++++++++- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 3 files changed, 224 insertions(+), 31 deletions(-) diff --git a/lib/controller/employee/employees_screen_controller.dart b/lib/controller/employee/employees_screen_controller.dart index 7d95421..a8d9f1a 100644 --- a/lib/controller/employee/employees_screen_controller.dart +++ b/lib/controller/employee/employees_screen_controller.dart @@ -21,6 +21,10 @@ class EmployeesScreenController extends GetxController { /// ✅ Upload state tracking (if needed later) RxMap uploadingStates = {}.obs; + RxList selectedEmployeePrimaryManagers = [].obs; + RxList selectedEmployeeSecondaryManagers = + [].obs; + @override void onInit() { super.onInit(); @@ -86,6 +90,52 @@ class EmployeesScreenController extends GetxController { isLoadingEmployeeDetails.value = false; } + /// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId + Future fetchReportingManagers(String? employeeId) async { + if (employeeId == null || employeeId.isEmpty) return; + + try { + // ✅ Always clear before new fetch (to avoid mixing old data) + selectedEmployeePrimaryManagers.clear(); + selectedEmployeeSecondaryManagers.clear(); + + // Fetch from existing API helper + final data = await ApiService.getOrganizationHierarchyList(employeeId); + + if (data == null || data.isEmpty) { + update(['employee_screen_controller']); + return; + } + + for (final item in data) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + + final emp = EmployeeModel.fromJson(reportTo); + final isPrimary = item['isPrimary'] == true; + + if (isPrimary) { + if (!selectedEmployeePrimaryManagers.any((e) => e.id == emp.id)) { + selectedEmployeePrimaryManagers.add(emp); + } + } else { + if (!selectedEmployeeSecondaryManagers.any((e) => e.id == emp.id)) { + selectedEmployeeSecondaryManagers.add(emp); + } + } + } catch (_) { + // ignore malformed items + } + } + + update(['employee_screen_controller']); + } catch (e) { + logSafe("Error fetching reporting managers for $employeeId", + level: LogLevel.error, error: e); + } + } + /// 🔹 Clear all employee data void clearEmployees() { employees.clear(); diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 7450384..baa59fa 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -38,6 +38,7 @@ class _EmployeeDetailPageState extends State with UIMixin { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { controller.fetchEmployeeDetails(widget.employeeId); + controller.fetchReportingManagers(widget.employeeId); }); } @@ -193,6 +194,7 @@ class _EmployeeDetailPageState extends State with UIMixin { child: MyRefreshIndicator( onRefresh: () async { await controller.fetchEmployeeDetails(widget.employeeId); + await controller.fetchReportingManagers(employee.id); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -298,10 +300,14 @@ class _EmployeeDetailPageState extends State with UIMixin { action: 0, ), hideMainSelector: true, - hideLoggedUserFromSelection: - true, + hideLoggedUserFromSelection: true, + loggedUserId: + controller.selectedEmployeeDetails.value?.id, ), ); + + // 🔄 Refresh reporting managers after editing + await controller.fetchReportingManagers(employee.id); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), @@ -326,6 +332,58 @@ class _EmployeeDetailPageState extends State with UIMixin { ), ), ), + Obx(() { + final primary = + controller.selectedEmployeePrimaryManagers; + final secondary = + controller.selectedEmployeeSecondaryManagers; + + if (primary.isEmpty && secondary.isEmpty) { + return const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + 'No reporting managers assigned', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.only( + top: 8.0, left: 8, right: 8, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'Primary → ${_getManagerNames(primary)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'Secondary → ${_getManagerNames(secondary)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + }) ], ), @@ -500,4 +558,13 @@ class _EmployeeDetailPageState extends State with UIMixin { }), ); } + + String _getManagerNames(List managers) { + if (managers.isEmpty) return '—'; + return managers + .map((m) => + '${(m.firstName ?? '').trim()} ${(m.lastName ?? '').trim()}'.trim()) + .where((name) => name.isNotEmpty) + .join(', '); + } } diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From 575fcc200c6215d3c17e91b5cee6a60276fc80db Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 09/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 - .../employees/employee_detail_screen.dart | 2 + .../manage_reporting_bottom_sheet.dart | 132 ++++-------------- 3 files changed, 30 insertions(+), 106 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 100c1dd..8f9f846 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -2,8 +2,6 @@ class ApiEndpoints { // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; - static const String baseUrl = "https://mapi.marcoaiot.com/api"; - static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index baa59fa..6492674 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,6 +13,8 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,36 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - // ignore if controller not found — not critical - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + // Keep sheet open and reset reporter selections for next assignment _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -426,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -527,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From 477667c06aa3d342c839fe3098b2d1ca3efff097 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 10/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 8f9f846..1a52cb4 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,7 +1,7 @@ class ApiEndpoints { // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; - // static const String baseUrl = "https://devapi.marcoaiot.com/api"; + static const String baseUrl = "https://devapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = From f3f1f022820adb25e2fd5442d2df92f7b9dbbcbb Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 11/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From 411e9afcc942a6eb76ec13f59be6a10c4062b072 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 11:30:36 +0530 Subject: [PATCH 12/60] .. --- lib/view/employees/manage_reporting_bottom_sheet.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..9bdf9f7 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -331,14 +331,17 @@ class _ManageReportingBottomSheetState await controller.fetchReportingManagers(empId); await controller.fetchEmployeeDetails(empId); } catch (_) { - // ignore if controller not found — not critical + } // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + _resetForm(); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } } else { showAppSnackbar( title: 'Error', From 899252f21517ec9ce7432c0c80832417e68b56fd Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 13/60] All Employees fetching task done in advance payment screen --- .../finance/advance_payment_controller.dart | 13 ++-- lib/helpers/services/api_service.dart | 59 +++++++++++-------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/lib/controller/finance/advance_payment_controller.dart b/lib/controller/finance/advance_payment_controller.dart index 1e159fc..84eb917 100644 --- a/lib/controller/finance/advance_payment_controller.dart +++ b/lib/controller/finance/advance_payment_controller.dart @@ -65,12 +65,17 @@ class AdvancePaymentController extends GetxController { try { employeesLoading.value = true; - final list = await ApiService.getEmployees(query: q); + // Build query params + final queryParams = { + 'allEmployee': 'true', + if (q.isNotEmpty) 'q': q, // only include search query if not empty + }; + + final list = await ApiService.getEmployees(queryParams: queryParams); final parsed = Employee.listFromJson(list); - logSafe("✅ Employees fetched from API: ${parsed.map((e) => e.name).toList()}"); + logSafe( + "✅ Employees fetched from API: ${parsed.map((e) => e.name).toList()}"); - - // Save full result and filter locally allEmployees = parsed; _filterEmployees(q); } catch (e, s) { diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index f2e8fbc..6061bcc 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -427,20 +427,35 @@ class ApiService { } /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); + try { + final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Service Project Detail request failed: null response", + level: LogLevel.error); + return null; + } if (response == null) { logSafe("Service Project Detail request failed: null response", level: LogLevel.error); return null; } + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -454,9 +469,19 @@ class ApiService { level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } return null; } + return null; + } /// Get Service Project List static Future getServiceProjectsListApi({ @@ -2091,34 +2116,22 @@ class ApiService { } /// Fetch employees with optional query. Returns raw list (List) - static Future> getEmployees({String query = ''}) async { + static Future> getEmployees( + {Map? queryParams}) async { try { - // endpoint relative to ApiEndpoints.baseUrl; _getRequest builds full url - var endpoint = ApiEndpoints.getEmployeesWithoutPermission; - Map? queryParams; - if (query.isNotEmpty) { - // server may expect a query param name other than 'q'. Adjust if needed. - queryParams = {'q': query}; - } + final endpoint = ApiEndpoints.getEmployeesWithoutPermission; final resp = await _getRequest(endpoint, queryParams: queryParams); if (resp == null) return []; - // parse response - try { - final body = jsonDecode(resp.body); - if (body is Map && body.containsKey('data')) { - final data = body['data']; - if (data is List) return data; - return []; - } else if (body is List) { - return body; - } else { - return []; - } - } catch (e, s) { - logSafe("❌ ApiService.getEmployees: parse error $e\n$s", - level: LogLevel.error); + final body = jsonDecode(resp.body); + if (body is Map && body.containsKey('data')) { + final data = body['data']; + if (data is List) return data; + return []; + } else if (body is List) { + return body; + } else { return []; } } catch (e, s) { From 36a3d586a0c08bf77cd649e616a6b127aae214d6 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 17:59:51 +0530 Subject: [PATCH 14/60] =?UTF-8?q?UI=20Enhancements=20in=20Finance=20Module?= =?UTF-8?q?=20=E2=80=93=20Payment=20Request=20&=20Expense=20Screens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/expense/expense_main_components.dart | 16 +++++++++++++++- lib/view/finance/payment_request_screen.dart | 3 ++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index 5218943..ae04ff6 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -343,7 +343,21 @@ class ExpenseList extends StatelessWidget { children: [ MyText.bodySmall(formattedDate, fontWeight: 500), const Spacer(), - MyText.bodySmall(expense.status.name, fontWeight: 500), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Color(int.parse( + '0xff${expense.status.color.substring(1)}')) + .withOpacity(0.5), + borderRadius: BorderRadius.circular(5), + ), + child: MyText.bodySmall( + expense.status.name, + color: Colors.white, + fontWeight: 500, + ), + ), ], ), ], diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index 8d1e6bd..417bfd4 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -383,7 +383,8 @@ class _PaymentRequestMainScreenState extends State const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Color(int.parse( - '0xff${item.expenseStatus.color.substring(1)}')), + '0xff${item.expenseStatus.color.substring(1)}')) + .withOpacity(0.5), borderRadius: BorderRadius.circular(5), ), child: MyText.bodySmall( From 61ca8f42509761fd5245805fe213d27ae26470b1 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 15/60] implementation of Manage reporting --- .../employees/employee_detail_screen.dart | 2 - .../manage_reporting_bottom_sheet.dart | 135 ++++-------------- 2 files changed, 28 insertions(+), 109 deletions(-) diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 6492674..baa59fa 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,8 +13,6 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; -import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 9bdf9f7..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,39 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - + // Keep sheet open and reset reporter selections for next assignment _resetForm(); - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -429,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -530,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From 509813ca2dc3a17dd04a25f775939b865bd8b6de Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 16/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From a61b9c50afba02d3422b5b7aef631c767aa4377b Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 17/60] implementation of Manage reporting --- .../manage_reporting_bottom_sheet.dart | 132 ++++-------------- 1 file changed, 28 insertions(+), 104 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,36 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - // ignore if controller not found — not critical - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + // Keep sheet open and reset reporter selections for next assignment _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -426,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -527,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From 8edf3e89a5f6a00ab1e036d63055e785c490531d Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 18/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From e024cf4576dbe26da1138c47e60b04efa4129240 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 11:30:36 +0530 Subject: [PATCH 19/60] .. --- lib/view/employees/manage_reporting_bottom_sheet.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..9bdf9f7 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -331,14 +331,17 @@ class _ManageReportingBottomSheetState await controller.fetchReportingManagers(empId); await controller.fetchEmployeeDetails(empId); } catch (_) { - // ignore if controller not found — not critical + } // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + _resetForm(); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } } else { showAppSnackbar( title: 'Error', From d3b8ce17b83bcde195de82ee4d1f08554a436dd7 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 20/60] All Employees fetching task done in advance payment screen --- lib/helpers/services/api_service.dart | 44 ++++++++------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 6061bcc..db01dd4 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -305,7 +305,6 @@ class ApiService { } } - // Service Project Module APIs /// Create a new Service Project Job static Future createServiceProjectJobApi({ @@ -427,35 +426,23 @@ class ApiService { } /// Get details of a single service project - static Future getServiceProjectDetailApi( - String projectId) async { - final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; - logSafe("Fetching details for Service Project ID: $projectId"); + /// Get details of a single service project static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); - try { - final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); - return null; - } - if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); + logSafe( + "Service Project Detail request failed: null response", + level: LogLevel.error, + ); return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -465,23 +452,18 @@ class ApiService { return ServiceProjectDetailModel.fromJson(jsonResponse); } } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", - level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); - } - if (jsonResponse != null) { - return ServiceProjectDetailModel.fromJson(jsonResponse); - } - } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", - level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); + logSafe( + "Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error, + ); + logSafe( + "StackTrace: $stack", + level: LogLevel.debug, + ); } return null; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({ From ffb02027ccbc127ba8622bf342ccd01d9a6bba00 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 17 Nov 2025 11:32:07 +0530 Subject: [PATCH 21/60] Rebase issues solved --- lib/helpers/services/api_endpoints.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 1a52cb4..44a7474 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,7 +1,7 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; - static const String baseUrl = "https://devapi.marcoaiot.com/api"; + //static const String baseUrl = "https://devapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = From 2149fd6bc51519a166b9c806039e0b4b5426d4b4 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 15:36:10 +0530 Subject: [PATCH 22/60] added service projet screen --- lib/helpers/services/api_endpoints.dart | 2 +- lib/view/service_project/service_project_screen.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 44a7474..91300af 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; //static const String baseUrl = "https://devapi.marcoaiot.com/api"; diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 8831369..505c914 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -273,9 +273,8 @@ 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), From b476986807ae8737a9cd9631cb1309a89f07d896 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 16:48:53 +0530 Subject: [PATCH 23/60] made chnages in details screen and list screen --- lib/view/service_project/service_project_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 505c914..8831369 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -273,8 +273,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), From 8dabac2effa6094193b40a4ee1727064c695bbd1 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 24/60] implementation of Manage reporting --- .../employees/employee_detail_screen.dart | 2 + .../manage_reporting_bottom_sheet.dart | 118 ++++-------------- 2 files changed, 28 insertions(+), 92 deletions(-) diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index baa59fa..6492674 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,6 +13,8 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 9bdf9f7..589be7d 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,14 +254,14 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers try { @@ -344,10 +283,9 @@ class _ManageReportingBottomSheetState } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -429,6 +367,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -530,21 +473,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From 01d7a2fd3be9555f3ab2800ef7e997b347e7010f Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 25/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 91300af..44a7474 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; //static const String baseUrl = "https://devapi.marcoaiot.com/api"; From 548ecb33a6eb5302986ba0fb4e9968a859a6f7dd Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 26/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 120 ++++++++++++++---- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 589be7d..9bdf9f7 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,14 +315,14 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers try { @@ -283,9 +344,10 @@ class _ManageReportingBottomSheetState } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -367,11 +429,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -473,12 +530,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From 6f8f1f1856f962a098fec87a06ea77b9c94da762 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 27/60] All Employees fetching task done in advance payment screen --- lib/helpers/services/api_service.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index db01dd4..7168892 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -427,11 +427,17 @@ class ApiService { /// Get details of a single service project /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); + try { + final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -442,7 +448,16 @@ class ApiService { ); return null; } + if (response == null) { + logSafe("Service Project Detail request failed: null response", + level: LogLevel.error); + return null; + } + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -461,9 +476,19 @@ class ApiService { level: LogLevel.debug, ); } + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } return null; } + return null; + } /// Get Service Project List static Future getServiceProjectsListApi({ From 55fffbbe78d8ce37f14ca86bdc867e6688fe5846 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 28/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 44a7474..f30c6be 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,7 +1,7 @@ class ApiEndpoints { static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; - //static const String baseUrl = "https://devapi.marcoaiot.com/api"; + // static const String baseUrl = "https://devapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = From 14ed2a8417e382a3efd638b01dd5f60cdb9b2665 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 29/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index f30c6be..1a52cb4 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,7 +1,7 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; - // static const String baseUrl = "https://devapi.marcoaiot.com/api"; + static const String baseUrl = "https://devapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = From d5868d213a34cfdc9323ea699e055fd749b388cc Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 17 Nov 2025 11:32:07 +0530 Subject: [PATCH 30/60] Rebase issues solved --- lib/helpers/services/api_endpoints.dart | 2 +- lib/helpers/services/api_service.dart | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 1a52cb4..91300af 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,7 +1,7 @@ class ApiEndpoints { // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; - static const String baseUrl = "https://devapi.marcoaiot.com/api"; + //static const String baseUrl = "https://devapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 7168892..d7379e0 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -436,8 +436,6 @@ class ApiService { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); - try { - final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -454,10 +452,6 @@ class ApiService { return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -487,8 +481,6 @@ class ApiService { return null; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({ From 79b727aeb1b8c8f588f79ccc4403c44d01bd8772 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 31/60] implementation of Manage reporting --- .../employees/employee_detail_screen.dart | 2 - .../manage_reporting_bottom_sheet.dart | 135 ++++-------------- 2 files changed, 28 insertions(+), 109 deletions(-) diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 6492674..baa59fa 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,8 +13,6 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; -import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 9bdf9f7..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,39 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - + // Keep sheet open and reset reporter selections for next assignment _resetForm(); - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -429,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -530,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From 3bae3429c6b0c49028a8e96480569e31ecbd95c7 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 15:36:10 +0530 Subject: [PATCH 32/60] added service projet screen --- lib/helpers/services/api_endpoints.dart | 2 +- lib/view/service_project/service_project_screen.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index fae25cf..203b936 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 50bdee7..7051705 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -218,9 +218,8 @@ 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), From 7fdeb103cfc27cdcdf26d7ff0f6b94562fb42c67 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 16:48:53 +0530 Subject: [PATCH 33/60] made chnages in details screen and list screen --- lib/view/service_project/service_project_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 7051705..50bdee7 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -218,8 +218,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), From 7f7d73c7905d04b67ed63330b23ffcc8ce47e1e3 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 34/60] implementation of Manage reporting --- lib/view/employees/employee_detail_screen.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index baa59fa..6492674 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,6 +13,8 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { From bcfa29c0edfb0b9a75db9467100fa8830a620224 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 35/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 203b936..fae25cf 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; From 8153d315b78c59da22b0f8abe1545c73704b886b Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 36/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 120 ++++++++++++++---- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..a5fc8de 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,14 +315,14 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); @@ -270,9 +331,10 @@ class _ManageReportingBottomSheetState _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +416,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +517,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From 0b894a5091f4703581be4f3431a2c0d14acf2b8c Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 37/60] All Employees fetching task done in advance payment screen --- lib/helpers/services/api_service.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 59a2d6c..b92741a 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -504,20 +504,35 @@ class ApiService { } /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); + try { + final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Service Project Detail request failed: null response", + level: LogLevel.error); + return null; + } if (response == null) { logSafe("Service Project Detail request failed: null response", level: LogLevel.error); return null; } + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -531,9 +546,19 @@ class ApiService { level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } return null; } + return null; + } /// Get Service Project List static Future getServiceProjectsListApi({ From 29af4e53a93072434392aad22d122fbf29feca10 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 38/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index fae25cf..203b936 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; From 793eeec7faf44bc0b7fa17a3184298e86896b687 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 17 Nov 2025 11:32:07 +0530 Subject: [PATCH 39/60] Rebase issues solved --- lib/helpers/services/api_service.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index b92741a..b7985c6 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -513,8 +513,6 @@ class ApiService { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); - try { - final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -529,10 +527,6 @@ class ApiService { return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -557,8 +551,6 @@ class ApiService { return null; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({ From 68ed97b64a87a7cb134d0bf68a5d62dbf3a7ec32 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 17 Nov 2025 11:32:07 +0530 Subject: [PATCH 40/60] Rebase issues solved --- lib/helpers/services/api_endpoints.dart | 2 +- lib/helpers/services/api_service.dart | 34 +++++++++---------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 203b936..fae25cf 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index b7985c6..d374a78 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -504,10 +504,6 @@ class ApiService { } /// Get details of a single service project - static Future getServiceProjectDetailApi( - String projectId) async { - final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; - logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; @@ -517,13 +513,10 @@ class ApiService { final response = await _getRequest(endpoint); if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); - return null; - } - if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); + logSafe( + "Service Project Detail request failed: null response", + level: LogLevel.error, + ); return null; } @@ -536,17 +529,14 @@ class ApiService { return ServiceProjectDetailModel.fromJson(jsonResponse); } } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", - level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); - } - if (jsonResponse != null) { - return ServiceProjectDetailModel.fromJson(jsonResponse); - } - } catch (e, stack) { - logSafe("Exception during getServiceProjectDetailApi: $e", - level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); + logSafe( + "Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error, + ); + logSafe( + "StackTrace: $stack", + level: LogLevel.debug, + ); } return null; From cddc2990fbeffa9562e5e8c92ba9113604db73bf Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 14 Nov 2025 15:35:41 +0530 Subject: [PATCH 41/60] created new emploee selector bottomsheet --- ...ice_project_details_screen_controller.dart | 6 +- .../service_project_details_screen.dart | 149 ++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/lib/controller/service_project/service_project_details_screen_controller.dart b/lib/controller/service_project/service_project_details_screen_controller.dart index 6a3f1d6..b09a150 100644 --- a/lib/controller/service_project/service_project_details_screen_controller.dart +++ b/lib/controller/service_project/service_project_details_screen_controller.dart @@ -52,12 +52,14 @@ class ServiceProjectDetailsController extends GetxController { errorMessage.value = ''; try { - final result = await ApiService.getServiceProjectDetailApi(projectId.value); + final result = + await ApiService.getServiceProjectDetailApi(projectId.value); if (result != null && result.data != null) { projectDetail.value = result.data!; } else { - errorMessage.value = result?.message ?? "Failed to fetch project details"; + errorMessage.value = + result?.message ?? "Failed to fetch project details"; } } catch (e) { errorMessage.value = "Error: $e"; diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart index 6e1dc36..33f6311 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -315,6 +315,155 @@ class _ServiceProjectDetailsScreenState ); } + Widget _buildJobsTab() { + return Obx(() { + if (controller.isJobLoading.value && controller.jobList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.jobErrorMessage.value.isNotEmpty && + controller.jobList.isEmpty) { + return Center( + child: MyText.bodyMedium(controller.jobErrorMessage.value)); + } + + if (controller.jobList.isEmpty) { + return Center(child: MyText.bodyMedium("No jobs found")); + } + + return ListView.separated( + controller: _jobScrollController, + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: controller.jobList.length + 1, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + if (index == controller.jobList.length) { + return controller.hasMoreJobs.value + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center(child: CircularProgressIndicator()), + ) + : const SizedBox.shrink(); + } + + final job = controller.jobList[index]; + return Card( + elevation: 3, + shadowColor: Colors.black26, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Job Title + MyText.titleMedium(job.title, fontWeight: 700), + MySpacing.height(6), + + // Job Description + MyText.bodySmall( + job.description.isNotEmpty + ? job.description + : "No description provided", + color: Colors.grey[700], + ), + + // Tags + if (job.tags != null && job.tags!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Wrap( + spacing: 2, + runSpacing: 4, + children: job.tags!.map((tag) { + return Chip( + label: Text( + tag.name, + style: const TextStyle(fontSize: 12), + ), + backgroundColor: Colors.grey[200], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ); + }).toList(), + ), + ), + + MySpacing.height(8), + + // Assignees & Status + Row( + children: [ + if (job.assignees != null && job.assignees!.isNotEmpty) + ...job.assignees!.map((assignee) { + return Padding( + padding: const EdgeInsets.only(right: 6), + child: CircleAvatar( + radius: 12, + backgroundImage: assignee.photo.isNotEmpty + ? NetworkImage(assignee.photo) + : null, + child: assignee.photo.isEmpty + ? Text(assignee.firstName[0]) + : null, + ), + ); + }).toList(), + ], + ), + + MySpacing.height(8), + + // Date Row with Status Chip + Row( + children: [ + // Dates (same as existing) + const Icon(Icons.calendar_today_outlined, + size: 14, color: Colors.grey), + MySpacing.width(4), + Text( + "${DateTimeUtils.convertUtcToLocal(job.startDate, format: 'dd MMM yyyy')} to " + "${DateTimeUtils.convertUtcToLocal(job.dueDate, format: 'dd MMM yyyy')}", + style: + const TextStyle(fontSize: 12, color: Colors.grey), + ), + + const Spacer(), + + // Status Chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: job.status.name.toLowerCase() == 'completed' + ? Colors.green[100] + : Colors.orange[100], + borderRadius: BorderRadius.circular(5), + ), + child: Text( + job.status.displayName, + style: TextStyle( + fontSize: 12, + color: job.status.name.toLowerCase() == 'completed' + ? Colors.green[800] + : Colors.orange[800], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }); + } + Widget _buildJobsTab() { return Obx(() { if (controller.isJobLoading.value && controller.jobList.isEmpty) { From fdd2d51ed0cef61f5ebdcfac38560ecda0a29ff9 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 42/60] implementation of manage reporting inside employee profile --- .../employees/manage_reporting_bottom_sheet.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index a5fc8de..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -324,10 +324,20 @@ class _ManageReportingBottomSheetState type: SnackbarType.success, ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( From e9a43af35065de33f31f534de226c2807e52780d Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 43/60] implementation of Manage reporting --- .../manage_reporting_bottom_sheet.dart | 132 ++++-------------- 1 file changed, 28 insertions(+), 104 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,36 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - // ignore if controller not found — not critical - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + // Keep sheet open and reset reporter selections for next assignment _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -426,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -527,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From ba6d58fd0a10262b4d1f7afe43accc08d36c92c9 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 44/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From 1f694381a1d5e25642b9762802f01bf67f7b3d53 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 11:30:36 +0530 Subject: [PATCH 45/60] .. --- lib/view/employees/manage_reporting_bottom_sheet.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..9bdf9f7 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -331,14 +331,17 @@ class _ManageReportingBottomSheetState await controller.fetchReportingManagers(empId); await controller.fetchEmployeeDetails(empId); } catch (_) { - // ignore if controller not found — not critical + } // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + _resetForm(); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } } else { showAppSnackbar( title: 'Error', From 58c6a3f9af328b27960fc6400e50f7e044cafb45 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 46/60] All Employees fetching task done in advance payment screen --- lib/helpers/services/api_service.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index d374a78..33440af 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -504,11 +504,17 @@ class ApiService { } /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); + try { + final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -519,7 +525,16 @@ class ApiService { ); return null; } + if (response == null) { + logSafe("Service Project Detail request failed: null response", + level: LogLevel.error); + return null; + } + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -538,9 +553,19 @@ class ApiService { level: LogLevel.debug, ); } + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } return null; } + return null; + } /// Get Service Project List static Future getServiceProjectsListApi({ From b8aefc544f5cc5bc77a210df3822ebbb1582e8ba Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 47/60] implementation of Manage reporting --- .../employees/employee_detail_screen.dart | 2 - .../manage_reporting_bottom_sheet.dart | 135 ++++-------------- 2 files changed, 28 insertions(+), 109 deletions(-) diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 6492674..baa59fa 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,8 +13,6 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; -import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 9bdf9f7..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,39 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - + // Keep sheet open and reset reporter selections for next assignment _resetForm(); - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -429,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -530,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From e3fe3c0d8658030ca30fd5274a0ba980bcf6bba9 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 48/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From 2d35addeebdc5a05644cbde65434e80ef115d945 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 49/60] implementation of Manage reporting --- .../manage_reporting_bottom_sheet.dart | 132 ++++-------------- 1 file changed, 28 insertions(+), 104 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..3eb5dd4 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,36 +254,25 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); - // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers - try { - final empId = employeeId; - final EmployeesScreenController controller = Get.find(); - await controller.fetchReportingManagers(empId); - await controller.fetchEmployeeDetails(empId); - } catch (_) { - // ignore if controller not found — not critical - } - - // Optional: re-fetch the organization hierarchy list (if needed elsewhere) + // Optionally refresh the saved hierarchy (not necessary here) but we can call: await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + // Keep sheet open and reset reporter selections for next assignment _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -426,6 +354,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -527,21 +460,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From be835bd2bd0ae3a417ac6425c26863466094eeb0 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 50/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From b9195c7fdd35b36e3e4c6ae8ef193abd0f6530d4 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 11:30:36 +0530 Subject: [PATCH 51/60] .. --- lib/view/employees/manage_reporting_bottom_sheet.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index ba035c6..9bdf9f7 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -331,14 +331,17 @@ class _ManageReportingBottomSheetState await controller.fetchReportingManagers(empId); await controller.fetchEmployeeDetails(empId); } catch (_) { - // ignore if controller not found — not critical + } // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset selections + _resetForm(); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } } else { showAppSnackbar( title: 'Error', From a8b31b3a95e134a29c5f79d2989d11e7a6e8d046 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 52/60] All Employees fetching task done in advance payment screen --- lib/helpers/services/api_service.dart | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 33440af..5e0772f 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -504,17 +504,12 @@ class ApiService { } /// Get details of a single service project - static Future getServiceProjectDetailApi( - String projectId) async { - final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; - logSafe("Fetching details for Service Project ID: $projectId"); + /// Get details of a single service project static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); - try { - final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -531,10 +526,6 @@ class ApiService { return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -564,8 +555,6 @@ class ApiService { return null; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({ From 01f3cd24da434a6320b58606a0d9585bf8dcc9d6 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 15:36:10 +0530 Subject: [PATCH 53/60] added service projet screen --- lib/helpers/services/api_endpoints.dart | 2 +- lib/view/service_project/service_project_screen.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index fae25cf..203b936 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 50bdee7..7051705 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -218,9 +218,8 @@ 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), From e1b6794e15a792fcd8a55ec556fd357cbc6474fe Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 16:48:53 +0530 Subject: [PATCH 54/60] made chnages in details screen and list screen --- lib/view/service_project/service_project_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 7051705..50bdee7 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -218,8 +218,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), From 58ed2676e32874d6a133a8a86e801a488a3dd7d7 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 55/60] implementation of Manage reporting --- .../employees/employee_detail_screen.dart | 2 + .../manage_reporting_bottom_sheet.dart | 118 ++++-------------- 2 files changed, 28 insertions(+), 92 deletions(-) diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index baa59fa..6492674 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,6 +13,8 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 9bdf9f7..589be7d 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,16 +13,14 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; - final String? loggedUserId; + final bool hideLoggedUserFromSelection; // ✅ new const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, - this.loggedUserId, + this.hideLoggedUserFromSelection = false, // default false }); @override @@ -213,76 +211,21 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); return; } - if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); return; } - final employeeId = _selectedEmployee!.id; - - // === BUILD PAYLOAD (updated logic) === - - // fetch current assignments so we can deactivate old ones - List? currentAssignments; - try { - currentAssignments = - await ApiService.getOrganizationHierarchyList(employeeId); - } catch (_) { - currentAssignments = null; - } - - // helper sets of newly selected ids - final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); - final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); - final List> payload = []; - // 1) For current active assignments: if they are not in new selections -> add isActive:false - if (currentAssignments != null && currentAssignments.isNotEmpty) { - for (final item in currentAssignments) { - try { - final reportTo = item['reportTo']; - if (reportTo == null) continue; - final reportToId = reportTo['id'] as String?; - if (reportToId == null) continue; - final isPrimary = item['isPrimary'] == true; - final currentlyActive = - item['isActive'] == true || item['isActive'] == null; // be safe - - // if currently active and not included in new selection -> mark false - if (currentlyActive) { - if (isPrimary && !newPrimaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": true, - "isActive": false, - }); - } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { - payload.add({ - "reportToId": reportToId, - "isPrimary": false, - "isActive": false, - }); - } - } - } catch (_) { - // ignore malformed items (same behavior as before) - } - } - } - - // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -290,8 +233,6 @@ class _ManageReportingBottomSheetState "isActive": true, }); } - - // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -301,13 +242,11 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog( - const Center(child: CircularProgressIndicator()), - barrierDismissible: false, - ); + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + final employeeId = _selectedEmployee!.id; final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -315,14 +254,14 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); + setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success, - ); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers try { @@ -344,10 +283,9 @@ class _ManageReportingBottomSheetState } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error, - ); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); } } @@ -429,6 +367,11 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), ), ) else @@ -530,21 +473,12 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { - final isProfileEmployee = widget.initialEmployee != null && - emp.id == widget.initialEmployee!.id; - return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: isProfileEmployee - ? Colors.indigo.shade50 - : Colors.indigo.shade50, + backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - // Only show delete icon / action for non-profile employees - deleteIcon: isProfileEmployee - ? null - : const Icon(Icons.close, size: 16), - onDeleted: - isProfileEmployee ? null : () => selectedList.remove(emp), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), ); }).toList(), ); From ceee12df330958a92171c83dfdf03b9eac75b363 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 56/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 203b936..fae25cf 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; From 2df4afcea729eb39f0dd661f10693f5fb438cae5 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH 57/60] implementation of manage reporting inside employee profile --- .../manage_reporting_bottom_sheet.dart | 120 ++++++++++++++---- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 589be7d..9bdf9f7 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,14 +315,14 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers try { @@ -283,9 +344,10 @@ class _ManageReportingBottomSheetState } } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -367,11 +429,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -473,12 +530,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), ); From a910d37f2277a8f22a59b9e516490a42e897e3a5 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 13 Nov 2025 15:27:13 +0530 Subject: [PATCH 58/60] All Employees fetching task done in advance payment screen --- lib/helpers/services/api_service.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 5e0772f..0613c50 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -505,11 +505,17 @@ class ApiService { /// Get details of a single service project /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); + try { + final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -526,6 +532,10 @@ class ApiService { return null; } + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -555,6 +565,8 @@ class ApiService { return null; } + return null; + } /// Get Service Project List static Future getServiceProjectsListApi({ From ed88e0dfc9cd9223c4fd7f7fcf42e75b0f15329c Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 12:04:35 +0530 Subject: [PATCH 59/60] implementation of Manage reporting --- lib/helpers/services/api_endpoints.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index fae25cf..203b936 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; From 39abfa2260ad01c59c38fabde2a91ffa754b0f89 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 17 Nov 2025 11:32:07 +0530 Subject: [PATCH 60/60] Rebase issues solved --- lib/helpers/services/api_endpoints.dart | 2 +- lib/helpers/services/api_service.dart | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 203b936..fae25cf 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,5 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 0613c50..9b8e2b0 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -514,8 +514,6 @@ class ApiService { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; logSafe("Fetching details for Service Project ID: $projectId"); - try { - final response = await _getRequest(endpoint); try { final response = await _getRequest(endpoint); @@ -532,10 +530,6 @@ class ApiService { return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -565,8 +559,6 @@ class ApiService { return null; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({