2026-01-13 11:36:24 +05:30

669 lines
21 KiB
Dart

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 = <ChallengeItem>[].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<void> 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<ChallengeItem> 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<void> refreshOngoingChallenges() async {
await fetchOngoingChallenges(isRefresh: true);
}
void loadMoreOngoingChallenges() {
if (!isOngoingLoading.value && ongoingHasMoreData.value) {
fetchOngoingChallenges();
}
}
void clearOngoingSearch() {
ongoingSearchController.clear();
ongoingSearchText.value = "";
}
//----------------------------------------------------------------------------------------------------
//......Upcoming Challenges...........................................................................
//----------------------------------------------------------------------------------------------------
var upcomingChallenges = <ChallengeItem>[].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<void> 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<ChallengeItem> 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 = <ChallengeItem>[].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<void> 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<ChallengeItem> 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 = <ChallengeItem>[].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<void> 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<ChallengeItem> 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<int> isJoiningChallengeLoading = <int>[].obs;
var joinChallengeError = ''.obs;
Future<bool> 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<String, dynamic> 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<bool> 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 = <SingleUserLeaderboard>[].obs;
var isParticipantsLoading = false.obs;
var participantsCurrentPage = 1;
var participantsHasMoreData = true.obs;
var currentChallengeId = 0;
Future<void> 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<SingleUserLeaderboard> 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<DateTime?> selectedDate = Rx<DateTime?>(null);
Future<void> 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);
}
}