import 'dart:convert'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:onufitness/constants/api_endpoints.dart'; import 'package:onufitness/models/master_dropdowns/fitness_goals_dropdown_response_model.dart'; import 'package:onufitness/screens/goals/models/get_all_goals_response_model.dart'; import 'package:onufitness/services/api_services/base_api_services.dart'; import 'package:onufitness/utils/custom_sneakbar.dart'; class GoalController extends GetxController { @override void onInit() { super.onInit(); fetchAllExploreGoals(); fetchFitnessGoalTypes(); fetchAllJoinedGoals(); selectedDate.value = DateTime.now(); showSelectedDateToUI.value = formatDate(selectedDate.value!); valueController = TextEditingController(); } @override void onClose() { valueController.dispose(); super.onClose(); } //.............................. Tab bar.................................................................... var selectedTabIndex = 0.obs; void switchTab(int index) { selectedTabIndex.value = index; // Fetch joined goals when switching to joined tab if not already loaded if (index == 1 && joinedGoals.isEmpty) { fetchAllJoinedGoals(); } } //....................................................................................................................... //......Fetch Explore API Call.......................................................................................... //....................................................................................................................... GetAllGoalsResponseModel exploreGoalModel = GetAllGoalsResponseModel(); var isLoading = false.obs; var isLoadingMore = false.obs; var exploreGoals = [].obs; var filteredGoals = [].obs; var totalCount = 0.obs; var currentPage = 1.obs; var pageSize = 10.obs; var hasError = false.obs; var errorMessage = ''.obs; var hasMoreData = true.obs; Future fetchAllExploreGoals({ int page = 1, bool isRefresh = false, }) async { try { if (page == 1) { isLoading.value = true; hasError.value = false; if (isRefresh) { exploreGoals.clear(); filteredGoals.clear(); } } else { isLoadingMore.value = true; } currentPage.value = page; var url = "${ApiUrl.getAllExploreGoals}?PageNumber=$page&PageSize=${pageSize.value}"; if (selectedFitnessGoalId.value != 0) { url += "&SearchType=FitnessGoal&SearchValue=${selectedFitnessGoalId.value}"; } final response = await ApiBase.getRequest( extendedURL: url, sendHeaders: true, ); log('Response status: ${response.statusCode}'); log('Response body: ${response.body}'); if (response.statusCode == 200) { exploreGoalModel = getAllGoalsResponseModelFromJson(response.body); if (exploreGoalModel.isSuccess == true) { totalCount.value = exploreGoalModel.data!.totalCount ?? 0; List newGoals = exploreGoalModel.data!.items ?? []; if (page == 1) { exploreGoals.assignAll(newGoals); filteredGoals.assignAll(newGoals); } else { exploreGoals.addAll(newGoals); filteredGoals.addAll(newGoals); } hasMoreData.value = newGoals.length == pageSize.value; hasError.value = false; errorMessage.value = ''; } else { hasError.value = true; errorMessage.value = exploreGoalModel.message ?? 'Failed to fetch goals'; } } else { hasError.value = true; errorMessage.value = 'Server error: ${response.statusCode}'; } } catch (e) { log('Network error: $e'); hasError.value = true; errorMessage.value = 'Network error: ${e.toString()}'; } finally { isLoading.value = false; isLoadingMore.value = false; } } // Apply filter and reset pagination Future applyFilter() async { currentPage.value = 1; hasMoreData.value = true; exploreGoals.clear(); filteredGoals.clear(); await fetchAllExploreGoals(page: 1); } Future refreshFetchAllGoals() async { currentPage.value = 1; hasMoreData.value = true; exploreGoals.clear(); filteredGoals.clear(); await fetchAllExploreGoals(page: 1, isRefresh: true); } // Load more goals (pagination) Future loadMore() async { if (!isLoading.value && !isLoadingMore.value && hasMoreData.value) { await fetchAllExploreGoals(page: currentPage.value + 1); } } //....................................................................................................................... // API call for joined goals............................................................................................. //....................................................................................................................... var currentJoinedPage = 1.obs; var hasMoreJoinedData = true.obs; var joinedGoals = [].obs; var joinedGoalsCount = 0.obs; var joinedGoalsErrorMessage = ''.obs; var isLoadingMoreJoinedGoals = false.obs; var hasJoinedGoalsError = false.obs; GetAllGoalsResponseModel joinedGoalModel = GetAllGoalsResponseModel(); RxBool isJoinedGoalsLoading = false.obs; Future fetchAllJoinedGoals({ int page = 1, bool isRefresh = false, }) async { try { if (page == 1) { isJoinedGoalsLoading.value = true; hasJoinedGoalsError.value = false; if (isRefresh) { joinedGoals.clear(); } } else { isLoadingMoreJoinedGoals.value = true; } currentJoinedPage.value = page; var url = "${ApiUrl.getAllJoinedGoals}?PageNumber=$page&PageSize=${pageSize.value}"; if (selectedJoinedFitnessGoalId.value != 0) { url += "&SearchType=FitnessGoal&SearchValue=${selectedJoinedFitnessGoalId.value}"; } final response = await ApiBase.getRequest( extendedURL: url, sendHeaders: true, ); if (response.statusCode == 200) { joinedGoalModel = getAllGoalsResponseModelFromJson(response.body); if (joinedGoalModel.isSuccess == true) { joinedGoalsCount.value = joinedGoalModel.data!.totalCount ?? 0; List newJoinedGoals = joinedGoalModel.data!.items ?? []; if (page == 1) { joinedGoals.assignAll(newJoinedGoals); } else { joinedGoals.addAll(newJoinedGoals); } hasMoreJoinedData.value = newJoinedGoals.length == pageSize.value; hasJoinedGoalsError.value = false; joinedGoalsErrorMessage.value = ''; } else { hasJoinedGoalsError.value = true; joinedGoalsErrorMessage.value = joinedGoalModel.message ?? 'Failed to fetch joined goals'; } } else { hasJoinedGoalsError.value = true; joinedGoalsErrorMessage.value = 'Server error: ${response.statusCode}'; } } catch (e) { log('Network error fetching joined goals: $e'); hasJoinedGoalsError.value = true; joinedGoalsErrorMessage.value = 'Network error: ${e.toString()}'; } finally { isJoinedGoalsLoading.value = false; isLoadingMoreJoinedGoals.value = false; } } // Apply filter for joined goals and reset pagination Future applyJoinedGoalsFilter() async { currentJoinedPage.value = 1; hasMoreJoinedData.value = true; joinedGoals.clear(); await fetchAllJoinedGoals(page: 1); } Future refreshFetchJoinedGoals() async { currentJoinedPage.value = 1; hasMoreJoinedData.value = true; joinedGoals.clear(); await fetchAllJoinedGoals(page: 1, isRefresh: true); } // Load more joined goals (pagination) Future loadMoreJoinedGoals() async { if (!isJoinedGoalsLoading.value && !isLoadingMoreJoinedGoals.value && hasMoreJoinedData.value) { await fetchAllJoinedGoals(page: currentJoinedPage.value + 1); } } //....................................................................................................................... // Join to a Goal......................................................................................................... //....................................................................................................................... RxInt selectedGoalIDforJoinGoal = 0.obs; RxBool isJoinGoalLoading = false.obs; Future joinGoal() async { bool ret = false; isJoinGoalLoading(true); try { final response = await ApiBase.postRequest( extendedURL: ApiUrl.joinToTheGoal, body: {"goalID": selectedGoalIDforJoinGoal.value}, ); log('Join Goal Response: ${response.statusCode} | ${response.body}'); if (response.statusCode == 200 || response.statusCode == 201) { ret = true; // Refresh joined goals after successful join await refreshFetchJoinedGoals(); } else { String errorMsg = 'Failed to participate in goal'; try { final body = jsonDecode(response.body); if (body['message'] != null) { errorMsg = body['message']; } } catch (_) { errorMsg = 'Failed to participate in goal'; } customSnackbar(title: "Failed!", message: errorMsg); } } catch (e) { log("Join goal error: $e"); customSnackbar( title: "Error", message: "Something went wrong. Please try again later.", ); } finally { isJoinGoalLoading(false); } return ret; } //....................................................................................................................... //............... Fetch Fitness Goals List For dropdown................................................................ //....................................................................................................................... var isFitnessGoalsLoading = false.obs; var apiFitnessGoalsList = [].obs; RxList localFitnessGoalsList = [].obs; var selectedFitnessGoal = "".obs; RxInt selectedFitnessGoalId = 0.obs; var selectedJoinedFitnessGoal = "".obs; RxInt selectedJoinedFitnessGoalId = 0.obs; //... Future fetchFitnessGoalTypes() async { isFitnessGoalsLoading(true); try { var response = await ApiBase.getRequest( extendedURL: ApiUrl.fetchFitnessGoals, sendHeaders: false, ); if (response.statusCode == 200 || response.statusCode == 201) { var responseData = fitnessGoalsResponseModelFromJson(response.body); if (responseData.isSuccess == true) { apiFitnessGoalsList.assignAll(responseData.data ?? []); localFitnessGoalsList.clear(); localFitnessGoalsList.add("All"); for (var goal in responseData.data!) { localFitnessGoalsList.add(goal.fitnessGoalTitle.toString()); } // Set default selection for explore goals selectedFitnessGoal.value = "All"; selectedFitnessGoalId.value = 0; // Set default selection for joined goals selectedJoinedFitnessGoal.value = "All"; selectedJoinedFitnessGoalId.value = 0; } else { var responseData = fitnessGoalsResponseModelFromJson(response.body); customSnackbar( title: "Error", message: responseData.message ?? "Failed to load Fitness Goals", ); } } else { var responseData = fitnessGoalsResponseModelFromJson(response.body); customSnackbar( title: "Error", message: responseData.message ?? "Failed to load Fitness Goals", ); } } catch (e) { customSnackbar(title: "Error", message: "Failed to load Fitness Goals"); } finally { isFitnessGoalsLoading(false); } } //....................................................................................................................... // Submit goal Input...................................................................................................... //....................................................................................................................... TextEditingController valueController = TextEditingController(); RxString showSelectedDateToUI = "".obs; final Rx selectedTask = Rx(null); final Rx selectedDate = Rx(null); RxInt selectedTaskId = 0.obs; RxInt selectedGoalIDforTaskInput = 0.obs; Future selectDate(BuildContext context) async { final DateTime? pickedDate = await showDatePicker( context: context, initialDate: selectedDate.value ?? DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime.now(), ); if (!context.mounted) return; if (pickedDate != null) { final now = DateTime.now(); final DateTime selectedDateTime = DateTime( pickedDate.year, pickedDate.month, pickedDate.day, now.hour, now.minute, ); if (selectedDateTime != selectedDate.value) { updateSelectedDate(selectedDateTime); } } } String formatDate(DateTime date) { return "${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}/${date.year}"; } void updateSelectedDate(DateTime date) { selectedDate.value = date; showSelectedDateToUI.value = formatDate(date); log("selected date : $selectedDate"); } void updateSelectedTask(Task task) { selectedTask.value = task; selectedTaskId.value = task.goalTaskId!; log("Selecetd Task :${selectedTaskId.value}"); } void clearForm() { selectedTask.value = null; selectedTaskId.value = 0; selectedDate.value = DateTime.now(); showSelectedDateToUI.value = formatDate(selectedDate.value!); valueController.clear(); } //............................ RxBool isInputLoading = false.obs; Future inputGoalTask() async { isInputLoading(true); bool ret = false; try { final metricValue = double.tryParse(valueController.text.trim()); final trackingDate = selectedDate.value?.toUtc().toIso8601String(); log("Date : $trackingDate"); final requestBody = { 'goalID': selectedGoalIDforTaskInput.value, 'goalTaskID': selectedTaskId.value, 'metricValue': metricValue!, 'trackingDate': trackingDate, }; log(requestBody.toString()); final response = await ApiBase.postRequest( extendedURL: ApiUrl.inputGoalTask, body: requestBody, sendHeaders: true, ); log(response.body.toString()); if (response.statusCode == 200 || response.statusCode == 201) { ret = true; final Map responseBody = jsonDecode(response.body); if (responseBody['isSuccess'] == true && responseBody['data'] != null) { final updatedGoal = SingleGoalData.fromJson(responseBody['data']); // 🔁 Call clean update method updateGoalFromResponse(updatedGoal); final message = responseBody['message'] ?? 'Performance submitted successfully!'; customSnackbar(title: "Success", message: message, duration: 1); clearForm(); } else { ret = false; customSnackbar( title: "Failed", message: responseBody['message'] ?? 'Submission failed', duration: 2, ); } } else { ret = false; final Map responseBody = jsonDecode(response.body); customSnackbar( title: "Failed", message: responseBody['message'] ?? 'Submission failed', duration: 2, ); } } catch (e) { log('Input goal task error: $e'); customSnackbar( title: "Error", message: "Something went wrong. Please try again.", duration: 2, ); ret = false; } finally { isInputLoading(false); } return ret; } void updateGoalFromResponse(SingleGoalData updatedGoal) { final goalIndex = joinedGoals.indexWhere( (goal) => goal.goalId == updatedGoal.goalId, ); if (goalIndex != -1) { // Update goal progress joinedGoals[goalIndex].goalProgressPercentage = updatedGoal.goalProgressPercentage; // Update tasks if (updatedGoal.tasks != null) { for (var updatedTask in updatedGoal.tasks!) { final taskIndex = joinedGoals[goalIndex].tasks?.indexWhere( (task) => task.goalTaskId == updatedTask.goalTaskId, ); if (taskIndex != null && taskIndex != -1) { joinedGoals[goalIndex].tasks![taskIndex] = updatedTask; } } } joinedGoals.refresh(); } } }