import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:onufitness/services/logger_service.dart'; import 'package:get/get.dart'; import 'package:onufitness/constants/api_endpoints.dart'; import 'package:onufitness/screens/rise/models/get_challenges_response_model.dart'; import 'package:onufitness/screens/rise/models/participient_leaderboard_response_model.dart'; import 'package:onufitness/services/api_services/base_api_services.dart'; import 'package:onufitness/utils/custom_sneakbar.dart'; class RiseController extends GetxController { final logger = LoggerService(); @override void onInit() { upcomingSearchController = TextEditingController(); createdSearchController = TextEditingController(); joinedSearchController = TextEditingController(); inputValueController = TextEditingController(); ongoingSearchController = TextEditingController(); super.onInit(); fetchOngoingChallenges(); fetchUpcomingChallenges(); fetchCreatedChallenges(); fetchJoinedChallenges(); Future.delayed(Duration(milliseconds: 900), () { _ongoingInitialized = true; _upcomingInitialized = true; _createdInitialized = true; _joinedInitialized = true; ongoingSearchController.addListener(_onOngoingSearchChanged); upcomingSearchController.addListener(_onUpcomingSearchChanged); createdSearchController.addListener(_onCreatedSearchChanged); joinedSearchController.addListener(_onJoinedSearchChanged); }); } @override void onClose() { _ongoingSearchTimer?.cancel(); _upcomingSearchTimer?.cancel(); _createdSearchTimer?.cancel(); _joinedSearchTimer?.cancel(); ongoingSearchController.removeListener(_onOngoingSearchChanged); upcomingSearchController.removeListener(_onUpcomingSearchChanged); createdSearchController.removeListener(_onCreatedSearchChanged); joinedSearchController.removeListener(_onJoinedSearchChanged); ongoingSearchController.dispose(); upcomingSearchController.dispose(); createdSearchController.dispose(); joinedSearchController.dispose(); super.onClose(); } //---------------------------------------------------------------------------------------------------- //......Ongoing Challenges............................................................................ //---------------------------------------------------------------------------------------------------- var ongoingChallenges = [].obs; var isOngoingLoading = false.obs; var ongoingCurrentPage = 1; var ongoingHasMoreData = true.obs; TextEditingController ongoingSearchController = TextEditingController(); final ongoingSearchText = "".obs; Timer? _ongoingSearchTimer; bool _ongoingInitialized = false; void _onOngoingSearchChanged() { if (!_ongoingInitialized) return; _ongoingSearchTimer?.cancel(); _ongoingSearchTimer = Timer(Duration(milliseconds: 800), () { ongoingSearchText.value = ongoingSearchController.text; fetchOngoingChallenges(isRefresh: true); }); } Future fetchOngoingChallenges({bool isRefresh = false}) async { if (isRefresh) { ongoingCurrentPage = 1; ongoingChallenges.clear(); ongoingHasMoreData.value = true; } if (isOngoingLoading.value || !ongoingHasMoreData.value) return; isOngoingLoading.value = true; try { var url = "${ApiUrl.fetchOngoingChallenges}?PageNumber=$ongoingCurrentPage&pageSize=20"; if (ongoingSearchController.text.isNotEmpty || ongoingSearchText.value.isNotEmpty) { url += "&SearchType=Name&SearchValue=${ongoingSearchText.value}"; } final response = await ApiBase.getRequest(extendedURL: url); if (response.statusCode == 200) { final challengesResponse = challengesResponseModelFromJson( response.body, ); if (challengesResponse.isSuccess == true && challengesResponse.data != null) { List newChallenges = challengesResponse.data!.items ?? []; if (newChallenges.isNotEmpty) { if (isRefresh) { ongoingChallenges.value = newChallenges; } else { ongoingChallenges.addAll(newChallenges); } ongoingCurrentPage++; } else { ongoingHasMoreData.value = false; } } else { if (isRefresh) ongoingChallenges.clear(); } } else { customSnackbar( title: "Error", message: "Failed to fetch ongoing challenges", ); } } catch (e) { logger.error('Error fetching ongoing challenges: $e'); customSnackbar( title: "Error", message: "Failed to fetch ongoing challenges", ); } finally { isOngoingLoading.value = false; } } Future refreshOngoingChallenges() async { await fetchOngoingChallenges(isRefresh: true); } void loadMoreOngoingChallenges() { if (!isOngoingLoading.value && ongoingHasMoreData.value) { fetchOngoingChallenges(); } } void clearOngoingSearch() { ongoingSearchController.clear(); ongoingSearchText.value = ""; } //---------------------------------------------------------------------------------------------------- //......Upcoming Challenges........................................................................... //---------------------------------------------------------------------------------------------------- var upcomingChallenges = [].obs; var isUpcomingLoading = false.obs; var upcomingCurrentPage = 1; var upcomingHasMoreData = true.obs; TextEditingController upcomingSearchController = TextEditingController(); final upcomingSearchText = "".obs; Timer? _upcomingSearchTimer; bool _upcomingInitialized = false; void _onUpcomingSearchChanged() { if (!_upcomingInitialized) return; _upcomingSearchTimer?.cancel(); _upcomingSearchTimer = Timer(Duration(milliseconds: 800), () { upcomingSearchText.value = upcomingSearchController.text; fetchUpcomingChallenges(isRefresh: true); }); } Future fetchUpcomingChallenges({bool isRefresh = false}) async { if (isRefresh) { upcomingCurrentPage = 1; upcomingChallenges.clear(); upcomingHasMoreData.value = true; } if (isUpcomingLoading.value || !upcomingHasMoreData.value) return; isUpcomingLoading.value = true; try { var url = "${ApiUrl.fetchUpcomingChallenges}?PageNumber=$upcomingCurrentPage&pageSize=20"; if (upcomingSearchController.text.isNotEmpty || upcomingSearchText.value.isNotEmpty) { url += "&SearchType=Name&SearchValue=${upcomingSearchText.value}"; } final response = await ApiBase.getRequest(extendedURL: url); if (response.statusCode == 200) { final challengesResponse = challengesResponseModelFromJson( response.body, ); if (challengesResponse.isSuccess == true && challengesResponse.data != null) { List newChallenges = challengesResponse.data!.items ?? []; if (newChallenges.isNotEmpty) { if (isRefresh) { upcomingChallenges.value = newChallenges; } else { upcomingChallenges.addAll(newChallenges); } upcomingCurrentPage++; } else { upcomingHasMoreData.value = false; } } else { if (isRefresh) upcomingChallenges.clear(); } } else { customSnackbar( title: "Error", message: "Failed to fetch upcoming challenges", ); } } catch (e) { logger.error('Error fetching upcoming challenges: $e'); customSnackbar( title: "Error", message: "Failed to fetch upcoming challenges", ); } finally { isUpcomingLoading.value = false; } } void refreshUpcomingChallenges() async { await fetchUpcomingChallenges(isRefresh: true); } void loadMoreUpcomingChallenges() { if (!isUpcomingLoading.value && upcomingHasMoreData.value) { fetchUpcomingChallenges(); } } void clearUpcomingSearch() { upcomingSearchController.clear(); upcomingSearchText.value = ""; } //---------------------------------------------------------------------------------------------------- //......Created Challenges............................................................................ //---------------------------------------------------------------------------------------------------- var createdChallenges = [].obs; var isCreatedLoading = false.obs; var createdCurrentPage = 1; var createdHasMoreData = true.obs; TextEditingController createdSearchController = TextEditingController(); final createdSearchText = "".obs; Timer? _createdSearchTimer; bool _createdInitialized = false; void _onCreatedSearchChanged() { if (!_createdInitialized) return; _createdSearchTimer?.cancel(); _createdSearchTimer = Timer(Duration(milliseconds: 800), () { createdSearchText.value = createdSearchController.text; fetchCreatedChallenges(isRefresh: true); }); } Future fetchCreatedChallenges({bool isRefresh = false}) async { if (isRefresh) { createdCurrentPage = 1; createdChallenges.clear(); createdHasMoreData.value = true; } if (isCreatedLoading.value || !createdHasMoreData.value) return; isCreatedLoading.value = true; try { var url = "${ApiUrl.fetchCreatedbyMeChallenges}?PageNumber=$createdCurrentPage&PageSize=10"; if (createdSearchController.text.isNotEmpty || createdSearchText.value.isNotEmpty) { url += "&SearchType=Name&SearchValue=${createdSearchText.value}"; } final response = await ApiBase.getRequest(extendedURL: url); if (response.statusCode == 200) { final challengesResponse = challengesResponseModelFromJson( response.body, ); if (challengesResponse.isSuccess == true && challengesResponse.data != null) { List newChallenges = challengesResponse.data!.items ?? []; if (newChallenges.isNotEmpty) { if (isRefresh) { createdChallenges.value = newChallenges; } else { createdChallenges.addAll(newChallenges); } createdCurrentPage++; } else { createdHasMoreData.value = false; } } else { if (isRefresh) createdChallenges.clear(); } } else { customSnackbar(title: "Error", message: "Failed to fetch challenges"); } } catch (e) { logger.error('Error fetching created challenges: $e'); customSnackbar(title: "Error", message: "Failed to fetch challenges"); } finally { isCreatedLoading.value = false; } } void refreshCreatedChallenges() async { await fetchCreatedChallenges(isRefresh: true); } void loadMoreCreatedChallenges() { if (!isCreatedLoading.value && createdHasMoreData.value) { fetchCreatedChallenges(); } } void clearCreatedSearch() { createdSearchController.clear(); createdSearchText.value = ""; } //---------------------------------------------------------------------------------------------------- //......Joined Challenges list............................................................................. //---------------------------------------------------------------------------------------------------- var joinedChallenges = [].obs; var isJoinedLoading = false.obs; var joinedCurrentPage = 1; var joinedHasMoreData = true.obs; TextEditingController joinedSearchController = TextEditingController(); final joinedSearchText = "".obs; Timer? _joinedSearchTimer; bool _joinedInitialized = false; void _onJoinedSearchChanged() { if (!_joinedInitialized) return; _joinedSearchTimer?.cancel(); _joinedSearchTimer = Timer(Duration(milliseconds: 800), () { joinedSearchText.value = joinedSearchController.text; fetchJoinedChallenges(isRefresh: true); }); } Future fetchJoinedChallenges({bool isRefresh = false}) async { if (isRefresh) { joinedCurrentPage = 1; joinedChallenges.clear(); joinedHasMoreData.value = true; } if (isJoinedLoading.value || !joinedHasMoreData.value) return; isJoinedLoading.value = true; try { var url = "${ApiUrl.fetchJoinedChallenges}?PageNumber=$joinedCurrentPage&PageSize=10"; if (joinedSearchController.text.isNotEmpty || joinedSearchText.value.isNotEmpty) { url += "&SearchType=Name&SearchValue=${joinedSearchText.value}"; } final response = await ApiBase.getRequest(extendedURL: url); if (response.statusCode == 200) { final challengesResponse = challengesResponseModelFromJson( response.body, ); if (challengesResponse.isSuccess == true && challengesResponse.data != null) { List newChallenges = challengesResponse.data!.items ?? []; if (newChallenges.isNotEmpty) { if (isRefresh) { joinedChallenges.value = newChallenges; } else { joinedChallenges.addAll(newChallenges); } joinedCurrentPage++; } else { joinedHasMoreData.value = false; } } else { if (isRefresh) joinedChallenges.clear(); } } } catch (e) { logger.error('Error fetching joined challenges: $e'); } finally { isJoinedLoading.value = false; } } void refreshJoinedChallenges() async { await fetchJoinedChallenges(isRefresh: true); } void loadMoreJoinedChallenges() { if (!isJoinedLoading.value && joinedHasMoreData.value) { fetchJoinedChallenges(); } } void clearJoinedSearch() { joinedSearchController.clear(); joinedSearchText.value = ""; } // Join a Challenge...................................................................... RxList isJoiningChallengeLoading = [].obs; var joinChallengeError = ''.obs; Future joinChallenge({required int challengeID}) async { bool ret = false; try { isJoiningChallengeLoading.add(challengeID); joinChallengeError.value = ''; final response = await ApiBase.postRequest( extendedURL: ApiUrl.joinChallenge, body: {"challengeID": challengeID}, ); if (response.statusCode == 200 || response.statusCode == 201) { ret = true; customSnackbar( title: 'Success', message: 'Successfully joined the challenge!', duration: 1, ); } else { ret = false; Map responseData = json.decode(response.body); String errorMessage = responseData['message'] ?? 'An error occurred while joining the challenge'; joinChallengeError.value = errorMessage; customSnackbar(title: 'Error', message: errorMessage, duration: 1); } } catch (e) { logger.error('Error joining challenge: $e'); ret = false; String errorMessage = 'An error occurred while joining the challenge'; joinChallengeError.value = errorMessage; customSnackbar(title: 'Error', message: errorMessage, duration: 1); } finally { isJoiningChallengeLoading.remove(challengeID); } return ret; } //......Input Performance............................................................................ TextEditingController inputValueController = TextEditingController(); RxBool isChallengeInputLoading = false.obs; Future inputChallengeTaskTracking({ required int challengeID, required int challengeTaskID, required String metricValue, }) async { bool ret = false; isChallengeInputLoading(true); try { final requestBody = { "challengeID": challengeID, "challengeTaskID": challengeTaskID, "metricValue": double.tryParse(metricValue) ?? 0, "trackingDate": selectedDate.value?.toUtc().toIso8601String(), }; final response = await ApiBase.postRequest( extendedURL: ApiUrl.inputChallengeTask, body: requestBody, ); final responseData = jsonDecode(response.body); if (response.statusCode == 200 || response.statusCode == 201) { ret = true; inputValueController.clear(); customSnackbar( title: "Successful!", message: responseData['message'] ?? "Performance submitted successfully!", duration: 1, ); } else { ret = false; customSnackbar( title: "Failed!", message: responseData['message'] ?? "Performance submission failed.", ); } } catch (e) { logger.error('Error submitting challenge task: $e'); ret = false; customSnackbar( title: "Failed!", message: "An error occurred. Please try again.", ); } finally { isChallengeInputLoading(false); } return ret; } //---------------------------------------------------------------------------------------------------- //......Challenge Participants (Stride Participants)................................................ //---------------------------------------------------------------------------------------------------- var challengeParticipants = [].obs; var isParticipantsLoading = false.obs; var participantsCurrentPage = 1; var participantsHasMoreData = true.obs; var currentChallengeId = 0; Future fetchChallengeParticipants({ required int challengeId, bool isRefresh = false, }) async { if (isRefresh) { participantsCurrentPage = 1; challengeParticipants.clear(); participantsHasMoreData.value = true; currentChallengeId = challengeId; } // If it's a different challenge, reset everything if (currentChallengeId != challengeId) { participantsCurrentPage = 1; challengeParticipants.clear(); participantsHasMoreData.value = true; currentChallengeId = challengeId; } if (isParticipantsLoading.value || !participantsHasMoreData.value) return; isParticipantsLoading.value = true; try { var url = "${ApiUrl.fetchLeaderBoard}/$challengeId?PageNumber=$participantsCurrentPage&PageSize=10"; final response = await ApiBase.getRequest(extendedURL: url); if (response.statusCode == 200) { final participantsResponse = leaderboardParticipentListResponseModelFromJson(response.body); if (participantsResponse.isSuccess == true && participantsResponse.data != null) { List newParticipants = participantsResponse.data!.items ?? []; if (newParticipants.isNotEmpty) { if (isRefresh) { challengeParticipants.value = newParticipants; } else { challengeParticipants.addAll(newParticipants); } participantsCurrentPage++; } else { participantsHasMoreData.value = false; } } else { if (isRefresh) challengeParticipants.clear(); } } else { customSnackbar( title: 'Error', message: 'Failed to fetch challenge participants', duration: 2, ); } } catch (e) { logger.error('Error fetching challenge participants: $e'); customSnackbar( title: 'Error', message: 'Failed to fetch challenge participants', duration: 2, ); } finally { isParticipantsLoading.value = false; } } void refreshChallengeParticipants(int challengeId) async { await fetchChallengeParticipants(challengeId: challengeId, isRefresh: true); } void loadMoreChallengeParticipants() { if (!isParticipantsLoading.value && participantsHasMoreData.value && currentChallengeId != 0) { fetchChallengeParticipants(challengeId: currentChallengeId); } } //---------------------------------------------------------------------------------------------------- //......Common Logic.................................................................................. //---------------------------------------------------------------------------------------------------- RxString showSelectedDateToUI = "".obs; final Rx selectedDate = Rx(null); 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); } }