diff --git a/lib/controller/task_planning/daily_task_planning_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart index 540b7c7..f94af8b 100644 --- a/lib/controller/task_planning/daily_task_planning_controller.dart +++ b/lib/controller/task_planning/daily_task_planning_controller.dart @@ -131,7 +131,7 @@ class DailyTaskPlanningController extends GetxController { } /// Fetch Infra details and then tasks per work area - Future fetchTaskData(String? projectId) async { + Future fetchTaskData(String? projectId, {String? serviceId}) async { if (projectId == null) { logSafe("Project ID is null", level: LogLevel.warning); return; @@ -139,6 +139,7 @@ class DailyTaskPlanningController extends GetxController { isLoading.value = true; try { + // Fetch infra details final infraResponse = await ApiService.getInfraDetails(projectId); final infraData = infraResponse?['data'] as List?; @@ -159,11 +160,12 @@ class DailyTaskPlanningController extends GetxController { return Floor( id: floorJson['id'], floorName: floorJson['floorName'], - workAreas: (floorJson['workAreas'] as List).map((areaJson) { + workAreas: + (floorJson['workAreas'] as List).map((areaJson) { return WorkArea( id: areaJson['id'], areaName: areaJson['areaName'], - workItems: [], // Initially empty, will fill after tasks API + workItems: [], // Will fill after tasks API ); }).toList(), ); @@ -182,13 +184,17 @@ class DailyTaskPlanningController extends GetxController { ); }).toList(); - // Fetch tasks for each work area - await Future.wait(dailyTasks.expand((task) => task.buildings) + // Fetch tasks for each work area, passing serviceId only if selected + await Future.wait(dailyTasks + .expand((task) => task.buildings) .expand((b) => b.floors) .expand((f) => f.workAreas) .map((area) async { try { - final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id); + final taskResponse = await ApiService.getWorkItemsByWorkArea( + area.id, + // serviceId: serviceId, // <-- only pass if not null + ); final taskData = taskResponse?['data'] as List? ?? []; area.workItems.addAll(taskData.map((taskJson) { @@ -200,11 +206,13 @@ class DailyTaskPlanningController extends GetxController { ? ActivityMaster.fromJson(taskJson['activityMaster']) : null, workCategoryMaster: taskJson['workCategoryMaster'] != null - ? WorkCategoryMaster.fromJson(taskJson['workCategoryMaster']) + ? WorkCategoryMaster.fromJson( + taskJson['workCategoryMaster']) : null, plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(), completedWork: (taskJson['completedWork'] as num?)?.toDouble(), - todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(), + todaysAssigned: + (taskJson['todaysAssigned'] as num?)?.toDouble(), description: taskJson['description'] as String?, taskDate: taskJson['taskDate'] != null ? DateTime.tryParse(taskJson['taskDate']) @@ -221,7 +229,8 @@ class DailyTaskPlanningController extends GetxController { logSafe("Fetched infra and tasks for project $projectId", level: LogLevel.info); } catch (e, stack) { - logSafe("Error fetching daily task data", level: LogLevel.error, error: e, stackTrace: stack); + logSafe("Error fetching daily task data", + level: LogLevel.error, error: e, stackTrace: stack); } finally { isLoading.value = false; update(); diff --git a/lib/controller/tenant/service_controller.dart b/lib/controller/tenant/service_controller.dart new file mode 100644 index 0000000..f832157 --- /dev/null +++ b/lib/controller/tenant/service_controller.dart @@ -0,0 +1,43 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/model/tenant/tenant_services_model.dart'; + +class ServiceController extends GetxController { + List services = []; + Service? selectedService; + final isLoadingServices = false.obs; + + /// Fetch services assigned to a project + Future fetchServices(String projectId) async { + try { + isLoadingServices.value = true; + final response = await ApiService.getAssignedServices(projectId); + if (response != null) { + services = response.data; + logSafe("Services fetched: ${services.length}"); + } else { + logSafe("Failed to fetch services for project $projectId", + level: LogLevel.error); + } + } finally { + isLoadingServices.value = false; + update(); + } + } + + /// Select a service + void selectService(Service? service) { + selectedService = service; + update(); + } + + /// Clear selection + void clearSelection() { + selectedService = null; + update(); + } + + /// Current selected name + String get currentSelection => selectedService?.name ?? "All Services"; +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index f5588c1..7f8b6c4 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -93,4 +93,5 @@ class ApiEndpoints { static const String getAssignedOrganizations = "/project/get/assigned/organization"; + static const String getAssignedServices = "/Project/get/assigned/services"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 5d80efc..b4080c9 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -19,6 +19,7 @@ import 'package:marco/model/document/master_document_type_model.dart'; import 'package:marco/model/document/document_details_model.dart'; import 'package:marco/model/document/document_version_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; +import 'package:marco/model/tenant/tenant_services_model.dart'; class ApiService { static const bool enableLogs = true; @@ -278,6 +279,36 @@ class ApiService { return null; } + //// Get Services assigned to a Project + static Future getAssignedServices( + String projectId) async { + final endpoint = "${ApiEndpoints.getAssignedServices}/$projectId"; + logSafe("Fetching services assigned to projectId: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Assigned Services request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Assigned Services"); + + if (jsonResponse != null) { + return ServiceListResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getAssignedServices: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + static Future postLogsApi(List> logs) async { const endpoint = "${ApiEndpoints.uploadLogs}"; logSafe("Posting logs... count=${logs.length}"); @@ -1928,20 +1959,20 @@ class ApiService { ); } -static Future?> getAllEmployees({String? organizationId}) async { - var endpoint = ApiEndpoints.getAllEmployees; + static Future?> getAllEmployees( + {String? organizationId}) async { + var endpoint = ApiEndpoints.getAllEmployees; - // Add organization filter if provided - if (organizationId != null && organizationId.isNotEmpty) { - endpoint += "?organizationId=$organizationId"; + // Add organization filter if provided + if (organizationId != null && organizationId.isNotEmpty) { + endpoint += "?organizationId=$organizationId"; + } + + return _getRequest(endpoint).then( + (res) => res != null ? _parseResponse(res, label: 'All Employees') : null, + ); } - return _getRequest(endpoint).then( - (res) => res != null ? _parseResponse(res, label: 'All Employees') : null, - ); -} - - static Future?> getRoles() async => _getRequest(ApiEndpoints.getRoles).then( (res) => res != null ? _parseResponse(res, label: 'Roles') : null); diff --git a/lib/helpers/widgets/tenant/service_selector.dart b/lib/helpers/widgets/tenant/service_selector.dart new file mode 100644 index 0000000..61b4ad4 --- /dev/null +++ b/lib/helpers/widgets/tenant/service_selector.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/model/tenant/tenant_services_model.dart'; +import 'package:marco/controller/tenant/service_controller.dart'; + +class ServiceSelector extends StatelessWidget { + final ServiceController controller; + + /// Called whenever a new service is selected (including "All Services") + final Future Function(Service?)? onSelectionChanged; + + /// Optional height for the selector + final double? height; + + const ServiceSelector({ + super.key, + required this.controller, + this.onSelectionChanged, + this.height, + }); + + Widget _popupSelector({ + required String currentValue, + required List items, + }) { + return PopupMenuButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + onSelected: items.isEmpty + ? null + : (name) async { + Service? service = name == "All Services" + ? null + : controller.services.firstWhere((e) => e.name == name); + + controller.selectService(service); + + if (onSelectionChanged != null) { + await onSelectionChanged!(service); + } + }, + itemBuilder: (context) { + if (items.isEmpty || items.length == 1 && items[0] == "All Services") { + return [ + const PopupMenuItem( + enabled: false, + child: Center( + child: Text( + "No services found", + style: TextStyle(color: Colors.grey), + ), + ), + ), + ]; + } + return items + .map((e) => PopupMenuItem(value: e, child: MyText(e))) + .toList(); + }, + child: Container( + height: height, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + currentValue.isEmpty ? "No services found" : currentValue, + style: const TextStyle( + color: Colors.black87, + fontSize: 13, + height: 1.2, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 18), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Obx(() { + if (controller.isLoadingServices.value) { + return const Center(child: CircularProgressIndicator()); + } + + final serviceNames = controller.services.isEmpty + ? [] + : [ + "All Services", + ...controller.services.map((e) => e.name).toList(), + ]; + + final currentValue = + controller.services.isEmpty ? "" : controller.currentSelection; + + return _popupSelector( + currentValue: currentValue, + items: serviceNames, + ); + }); + } +} diff --git a/lib/model/tenant/tenant_services_model.dart b/lib/model/tenant/tenant_services_model.dart new file mode 100644 index 0000000..2416e38 --- /dev/null +++ b/lib/model/tenant/tenant_services_model.dart @@ -0,0 +1,78 @@ +class ServiceListResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final String timestamp; + + ServiceListResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ServiceListResponse.fromJson(Map json) { + return ServiceListResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: (json['data'] as List?) + ?.map((e) => Service.fromJson(e)) + .toList() ?? + [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: json['timestamp'] ?? '', + ); + } + + Map toJson() { + return { + 'success': success, + 'message': message, + 'data': data.map((e) => e.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp, + }; + } +} + +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() { + return { + 'id': id, + 'name': name, + 'description': description, + 'isSystem': isSystem, + 'isActive': isActive, + }; + } +} diff --git a/lib/view/taskPlanning/daily_progress.dart b/lib/view/taskPlanning/daily_progress.dart index 868ab7b..cfc2447 100644 --- a/lib/view/taskPlanning/daily_progress.dart +++ b/lib/view/taskPlanning/daily_progress.dart @@ -17,6 +17,8 @@ import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; +import 'package:marco/controller/tenant/service_controller.dart'; +import 'package:marco/helpers/widgets/tenant/service_selector.dart'; class DailyProgressReportScreen extends StatefulWidget { const DailyProgressReportScreen({super.key}); @@ -41,6 +43,7 @@ class _DailyProgressReportScreenState extends State final PermissionController permissionController = Get.find(); final ProjectController projectController = Get.find(); + final ServiceController serviceController = Get.put(ServiceController()); @override void initState() { @@ -131,8 +134,7 @@ class _DailyProgressReportScreenState extends State child: MyRefreshIndicator( onRefresh: _refreshData, child: CustomScrollView( - physics: - const AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverToBoxAdapter( child: GetBuilder( @@ -143,6 +145,26 @@ class _DailyProgressReportScreenState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(flexSpacing), + + // --- ADD SERVICE SELECTOR HERE --- + Padding( + padding: MySpacing.x(10), + child: ServiceSelector( + controller: serviceController, + height: 40, + onSelectionChanged: (service) async { + final projectId = + dailyTaskController.selectedProjectId; + if (projectId?.isNotEmpty ?? false) { + await dailyTaskController.fetchTaskData( + projectId!, + // serviceId: service?.id, + ); + } + }, + ), + ), + _buildActionBar(), Padding( padding: MySpacing.x(8), diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index b8bcb71..7411d28 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -13,6 +13,8 @@ import 'package:marco/model/dailyTaskPlanning/assign_task_bottom_sheet .dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; +import 'package:marco/controller/tenant/service_controller.dart'; +import 'package:marco/helpers/widgets/tenant/service_selector.dart'; class DailyTaskPlanningScreen extends StatefulWidget { DailyTaskPlanningScreen({super.key}); @@ -29,6 +31,7 @@ class _DailyTaskPlanningScreenState extends State final PermissionController permissionController = Get.put(PermissionController()); final ProjectController projectController = Get.find(); + final ServiceController serviceController = Get.put(ServiceController()); @override void initState() { @@ -143,6 +146,26 @@ class _DailyTaskPlanningScreenState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(10), + child: ServiceSelector( + controller: + serviceController, + height: 40, + onSelectionChanged: (service) async { + final projectId = + projectController.selectedProjectId.value; + if (projectId.isNotEmpty) { + await dailyTaskPlanningController.fetchTaskData( + projectId, + // serviceId: service + // ?.id, + ); + } + }, + ), + ), MySpacing.height(flexSpacing), Padding( padding: MySpacing.x(8),