521 lines
17 KiB
Dart
521 lines
17 KiB
Dart
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 = <SingleGoalData>[].obs;
|
|
var filteredGoals = <SingleGoalData>[].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<void> 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<SingleGoalData> 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<void> applyFilter() async {
|
|
currentPage.value = 1;
|
|
hasMoreData.value = true;
|
|
exploreGoals.clear();
|
|
filteredGoals.clear();
|
|
await fetchAllExploreGoals(page: 1);
|
|
}
|
|
|
|
Future<void> refreshFetchAllGoals() async {
|
|
currentPage.value = 1;
|
|
hasMoreData.value = true;
|
|
exploreGoals.clear();
|
|
filteredGoals.clear();
|
|
await fetchAllExploreGoals(page: 1, isRefresh: true);
|
|
}
|
|
|
|
// Load more goals (pagination)
|
|
Future<void> 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 = <SingleGoalData>[].obs;
|
|
var joinedGoalsCount = 0.obs;
|
|
var joinedGoalsErrorMessage = ''.obs;
|
|
var isLoadingMoreJoinedGoals = false.obs;
|
|
var hasJoinedGoalsError = false.obs;
|
|
|
|
GetAllGoalsResponseModel joinedGoalModel = GetAllGoalsResponseModel();
|
|
RxBool isJoinedGoalsLoading = false.obs;
|
|
|
|
Future<void> 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<SingleGoalData> 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<void> applyJoinedGoalsFilter() async {
|
|
currentJoinedPage.value = 1;
|
|
hasMoreJoinedData.value = true;
|
|
joinedGoals.clear();
|
|
await fetchAllJoinedGoals(page: 1);
|
|
}
|
|
|
|
Future<void> refreshFetchJoinedGoals() async {
|
|
currentJoinedPage.value = 1;
|
|
hasMoreJoinedData.value = true;
|
|
joinedGoals.clear();
|
|
await fetchAllJoinedGoals(page: 1, isRefresh: true);
|
|
}
|
|
|
|
// Load more joined goals (pagination)
|
|
Future<void> 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<bool> 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 = <SingleFitnessGoal>[].obs;
|
|
RxList<String> localFitnessGoalsList = <String>[].obs;
|
|
var selectedFitnessGoal = "".obs;
|
|
RxInt selectedFitnessGoalId = 0.obs;
|
|
var selectedJoinedFitnessGoal = "".obs;
|
|
RxInt selectedJoinedFitnessGoalId = 0.obs;
|
|
//...
|
|
Future<void> 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<Task?> selectedTask = Rx<Task?>(null);
|
|
final Rx<DateTime?> selectedDate = Rx<DateTime?>(null);
|
|
RxInt selectedTaskId = 0.obs;
|
|
RxInt selectedGoalIDforTaskInput = 0.obs;
|
|
|
|
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);
|
|
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<bool> 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<String, dynamic> 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<String, dynamic> 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();
|
|
}
|
|
}
|
|
}
|