onufitness_mobile/lib/screens/rise/controllers/create_challenge_controller.dart
2026-01-13 11:36:24 +05:30

844 lines
26 KiB
Dart

import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:onufitness/services/logger_service.dart';
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/models/master_dropdowns/metric_list_response_model.dart';
import 'package:onufitness/screens/echoboard/models/get_exclusive_connections_response_model.dart';
import 'package:onufitness/services/api_services/base_api_services.dart';
import 'package:onufitness/utils/custom_sneakbar.dart';
class CreateChallengeController extends GetxController {
final logger = LoggerService();
@override
void onInit() {
titleController = TextEditingController();
descriptionController = TextEditingController();
taskTitleController = TextEditingController();
metricsController = TextEditingController();
targetValueController = TextEditingController();
goalController = TextEditingController();
unitController = TextEditingController();
connectionSearchController = TextEditingController();
super.onInit();
}
@override
void onClose() {
titleController.dispose();
descriptionController.dispose();
taskTitleController.dispose();
metricsController.dispose();
targetValueController.dispose();
goalController.dispose();
unitController.dispose();
super.onClose();
}
void clearAll() {
titleController.clear();
descriptionController.clear();
taskTitleController.clear();
targetValueController.clear();
challengeVisibilityId.value = 1; // Reset to Public
selectedConnections.clear();
selectedIds.clear();
selectedConnections.clear();
selectedConnections.refresh();
selectedIds.clear();
selectedFitnessGoal.value = "";
selectedFitnessGoalId.value = 0;
startDate.value = null;
endDate.value = null;
demoVideoName.value = '';
demoVideoFile.value = null;
bannerImageName.value = '';
bannerImageFile.value = null;
selectedMetric.value = '';
selectedMetricId.value = 0;
selectedUnit.value = '';
localUnitsList.clear();
isAutoJoin.value = true;
}
// All TextEditingControllers
TextEditingController titleController = TextEditingController();
TextEditingController descriptionController = TextEditingController();
TextEditingController taskTitleController = TextEditingController();
TextEditingController metricsController = TextEditingController();
TextEditingController targetValueController = TextEditingController();
TextEditingController goalController = TextEditingController();
TextEditingController unitController = TextEditingController();
// Date controllers
Rx<DateTime?> startDate = Rx<DateTime?>(null);
Rx<DateTime?> endDate = Rx<DateTime?>(null);
// File picker observables
RxString demoVideoName = ''.obs;
RxString bannerImageName = ''.obs;
// File objects to store the actual files
Rx<PlatformFile?> demoVideoFile = Rx<PlatformFile?>(null);
Rx<PlatformFile?> bannerImageFile = Rx<PlatformFile?>(null);
// Loading states
RxBool isVideoUploading = false.obs;
RxBool isImageUploading = false.obs;
RxBool isAutoJoin = true.obs;
// Clear demo video
void clearDemoVideo() {
demoVideoFile.value = null;
demoVideoName.value = '';
}
// Clear banner image
void clearBannerImage() {
bannerImageFile.value = null;
bannerImageName.value = '';
}
// Date picker methods
Future<void> pickStartDate(BuildContext context) async {
final now = DateTime.now();
final currentStartDate = startDate.value;
final localStartDate = currentStartDate?.toLocal();
// Ensure initialDate is not before firstDate
final initialDate =
localStartDate != null && localStartDate.isAfter(now)
? localStartDate
: now;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: now,
lastDate: DateTime(2030),
);
if (picked != null) {
final combined = DateTime(
picked.year,
picked.month,
picked.day,
now.hour,
now.minute,
now.second,
now.millisecond,
now.microsecond,
);
startDate.value = combined.toUtc();
endDate.value = null;
}
}
Future<void> pickEndDate(BuildContext context) async {
final now = DateTime.now();
final currentEndDate = endDate.value;
final currentStartDate = startDate.value;
final firstDate =
currentStartDate != null
? DateTime(
currentStartDate.toLocal().year,
currentStartDate.toLocal().month,
currentStartDate.toLocal().day,
)
: now;
final localEndDate = currentEndDate?.toLocal();
final initialDate =
localEndDate != null && localEndDate.isAfter(firstDate)
? localEndDate
: firstDate;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: firstDate,
lastDate: DateTime(2030),
);
if (picked != null) {
final combined = DateTime(
picked.year,
picked.month,
picked.day,
now.hour,
now.minute,
now.second,
now.millisecond,
now.microsecond,
);
endDate.value = combined.toUtc();
}
}
// Validation methods
bool get isDemoVideoSelected => demoVideoName.value.isNotEmpty;
bool get isBannerImageSelected => bannerImageName.value.isNotEmpty;
// Create Challenge Form validation...................................................................
bool hasValidWordLength(String text) {
final wordCount = text.trim().split(RegExp(r'\s+')).length;
return wordCount <= 100;
}
bool hasValidWordLengthDescription(String text) {
final wordCount = text.trim().split(RegExp(r'\s+')).length;
return wordCount <= 255;
}
bool validateForm() {
final title = titleController.text.trim();
final taskTitle = taskTitleController.text.trim();
final description = descriptionController.text.trim();
if (!hasValidWordLength(title)) {
customSnackbar(
title: "Challenge Title Length Exceeded",
message: "Challenge Title should not exceed 100 words",
);
return false;
}
if (!hasValidWordLength(taskTitle)) {
customSnackbar(
title: "Task Title Length Exceeded",
message: "Task Title should not exceed 100 words",
duration: 2,
);
return false;
}
if (!hasValidWordLengthDescription(description)) {
customSnackbar(
title: "Description Length Exceeded",
message: "Description should not exceed 100 words",
duration: 2,
);
return false;
}
if (titleController.text.trim().isEmpty) {
customSnackbar(
title: "Validation Error",
message: "Please enter challenge title",
duration: 2,
);
return false;
}
if (selectedFitnessGoalId.value == 0) {
customSnackbar(
title: "Validation Error",
message: "Please select a fitness goal",
duration: 2,
);
return false;
}
if (startDate.value == null) {
customSnackbar(
title: "Validation Error",
message: "Please select start date",
duration: 2,
);
return false;
}
if (endDate.value == null) {
customSnackbar(
title: "Validation Error",
message: "Please select end date",
duration: 2,
);
return false;
}
if (descriptionController.text.trim().isEmpty) {
customSnackbar(
title: "Validation Error",
message: "Please enter description",
duration: 2,
);
return false;
}
if (taskTitleController.text.trim().isEmpty) {
customSnackbar(
title: "Validation Error",
message: "Please enter task title",
duration: 2,
);
return false;
}
if (selectedMetricId.value == 0) {
customSnackbar(
title: "Validation Error",
message: "Please select a metric",
duration: 2,
);
return false;
}
if (targetValueController.text.trim().isEmpty) {
customSnackbar(
title: "Validation Error",
message: "Please enter target value",
duration: 2,
);
return false;
}
if (selectedUnit.value.isEmpty) {
customSnackbar(
title: "Validation Error",
message: "Please select a unit",
duration: 2,
);
return false;
}
if (challengeVisibilityId.value == 3) {
if (selectedConnections.isEmpty) {
customSnackbar(
title: "Validation Error",
message: "Please select at least one member or group",
duration: 2,
);
return false;
}
}
return true;
}
//..................................................................................................
// Calculate duration in days......................................................................
int calculateDuration() {
if (startDate.value != null && endDate.value != null) {
return endDate.value!.difference(startDate.value!).inDays + 1;
}
return 0;
}
//......................................................................................................
//......................................................................................................
// Challenge Privacy Update API Call.....................................................................
//......................................................................................................
//......................................................................................................
RxBool isUpdatePrivacyLoading = false.obs;
Future<bool> updateChallengePrivacy({
required int selectedChallengeId,
}) async {
bool ret = false;
isUpdatePrivacyLoading.value = true;
try {
// Prepare form data
Map<String, dynamic> formData = {};
formData['ChallengeID'] = selectedChallengeId.toString();
formData['ChallengeVisibilityID'] =
challengeVisibilityId.value.toString();
// Prepare new connections JSON (only for exclusive)
if (challengeVisibilityId.value == 3) {
formData['NewChallengeConnectionJson'] = jsonEncode(
selectedConnections,
);
} else {
// If switching to public, we need to remove all connections
formData['NewChallengeConnectionJson'] = jsonEncode([]);
}
//Always keep deleteJson empty
formData['DeleteChallengeConnectionJson'] = jsonEncode([]);
final response = await ApiBase.patchAnyFileWithMimeType(
extendedURL: ApiUrl.updateChallengePrivacy,
body: formData,
sendHeaders: true,
);
if (response.statusCode == 200 || response.statusCode == 201) {
ret = true;
customSnackbar(
title: "Success",
message: "Challenge privacy updated successfully!",
);
// Clear selections after successful update
selectedConnections.clear();
selectedIds.clear();
} else {
ret = false;
try {
final errorData = jsonDecode(response.body);
final errorMessage =
errorData['message'] ??
"Failed to update challenge privacy. Please try again.";
customSnackbar(title: "Error", message: errorMessage);
} catch (e) {
ret = false;
customSnackbar(
title: "Error",
message: "Failed to update challenge privacy. Please try again.",
);
}
}
} catch (e) {
logger.error('Error updating challenge privacy: $e');
ret = false;
customSnackbar(
title: "Error",
message: "Something went wrong. Please try again.",
);
} finally {
isUpdatePrivacyLoading.value = false;
}
return ret;
}
//................................................................................................
// Create Challenge API Call -......................................................................
//................................................................................................
RxInt challengeVisibilityId = 1.obs; // Default to Public (1)
RxBool isCreatingChallengeLoading = false.obs;
Future<bool> createChallenge() async {
bool ret = false;
if (!validateForm()) return false;
isCreatingChallengeLoading.value = true;
try {
// Prepare form data (body fields)
Map<String, dynamic> formData = {};
formData['ChallengeTitle'] = titleController.text.trim().toString();
formData['ChallengeDescription'] =
descriptionController.text.trim().toString();
formData['FitnessGoalID'] =
selectedFitnessGoalId.value.toString().toString();
formData['StartDate'] = startDate.value!.toIso8601String().toString();
formData['EndDate'] = endDate.value!.toIso8601String().toString();
formData['Duration'] = calculateDuration().toString().toString();
formData['IsAutoJoin'] = isAutoJoin.value.toString();
formData['ChallengeVisibilityID'] =
challengeVisibilityId.value.toString();
// Challenge tasks JSON
List<Map<String, dynamic>> challengeTasks = [
{
"taskTitle": taskTitleController.text.trim(),
"taskMetricID": selectedMetricId.value,
"taskUnit": selectedUnit.value,
"targetValue": int.tryParse(targetValueController.text.trim()) ?? 0,
},
];
formData['ChallengeTasks'] = jsonEncode(challengeTasks);
// Challenge connections JSON
if (challengeVisibilityId.value == 3) {
formData['ChallengeConnectionsJson'] = jsonEncode(selectedConnections);
}
// Prepare files map with different field names
Map<String, dynamic> filesMap = {};
// Add video file if exists
if (demoVideoFile.value != null) {
filesMap['ChallengeVideo'] = demoVideoFile.value!;
}
// Add image file if exists
if (bannerImageFile.value != null) {
filesMap['ChallengeImage'] = bannerImageFile.value!;
}
final response = await ApiBase.postMultipleIndivisualFiles(
extendedURL: ApiUrl.createChallenge,
body: formData,
files: filesMap,
sendHeaders: true,
);
if (response.statusCode == 200 || response.statusCode == 201) {
ret = true;
customSnackbar(
title: "Success",
message: "Challenge created successfully!",
);
await Future.delayed(Duration(milliseconds: 700));
clearForm();
Get.back();
} else {
ret = false;
try {
ret = false;
final errorData = jsonDecode(response.body);
final errorMessage =
errorData['message'] ??
"Failed to create challenge. Please try again.";
customSnackbar(title: "Error", message: errorMessage);
} catch (e) {
ret = false;
customSnackbar(
title: "Error",
message: "Failed to create challenge. Please try again.",
);
}
}
} catch (e) {
logger.error('Error creating challenge: $e');
ret = false;
customSnackbar(
title: "Error",
message: "Something went wrong. Please try again.",
);
} finally {
isCreatingChallengeLoading.value = false;
}
isCreatingChallengeLoading.value = false;
return ret;
}
// Clear form......................................................................................
void clearForm() {
challengeVisibilityId.value = 1; // Reset to Public
selectedConnections.clear();
selectedIds.clear();
titleController.clear();
descriptionController.clear();
taskTitleController.clear();
targetValueController.clear();
startDate.value = null;
endDate.value = null;
demoVideoFile.value = null;
demoVideoName.value = '';
bannerImageFile.value = null;
bannerImageName.value = '';
selectedFitnessGoal.value = '';
selectedFitnessGoalId.value = 0;
selectedMetric.value = '';
selectedMetricId.value = 0;
selectedUnit.value = '';
selectedConnections.clear();
selectedIds.clear();
}
// Add Participent API Call ..............................................................................
RxBool isAddPrticipentLoading = false.obs;
Future<bool> addParticipentToChallenge({
required int selectedChallengeId,
}) async {
bool ret = false;
isAddPrticipentLoading(true);
try {
final response = await ApiBase.postRequest(
extendedURL: ApiUrl.addParticipentToChallenge,
body: {
"challengeID": selectedChallengeId,
"challengeConnectionsJson": jsonEncode(selectedConnections),
},
);
if (response.statusCode == 200 || response.statusCode == 201) {
customSnackbar(
title: "Success",
message: "Participants added successfully",
duration: 1,
);
ret = true;
} else {
final data = jsonDecode(response.body);
ret = false;
customSnackbar(
title: "Error",
message:
data['message'] ??
"Failed to add participants. Please try again.",
);
}
} catch (e) {
logger.error('Error adding participants to challenge: $e');
ret = false;
customSnackbar(
title: "Error",
message: "Something went wrong. Please try again later.",
);
} finally {
isAddPrticipentLoading(false);
}
return ret;
}
//.......................................................................................................
//............... Fetch Fitness Goals List................................................................
//.......................................................................................................
var isFitnessGoalsLoading = false.obs;
var apiFitnessGoalsList = <SingleFitnessGoal>[].obs;
var selectedFitnessGoal = "".obs;
RxList<String> localFitnessGoalsList = <String>[].obs;
RxInt selectedFitnessGoalId = 0.obs;
Future<void> fetchFitnessGoals() 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();
for (var goal in responseData.data!) {
localFitnessGoalsList.add(goal.fitnessGoalTitle.toString());
}
} 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) {
logger.error('Error fetching fitness goals: $e');
customSnackbar(title: "Error", message: "Failed to load Fitness Goals");
} finally {
isFitnessGoalsLoading(false);
}
}
//.......................................................................................................
//... Fetch Metrics and Units............................................................................
//.......................................................................................................
var isMetricsLoading = false.obs;
var apiMetricsList = <SingelMetricModel>[].obs;
var localMetricsList = <String>[].obs;
var selectedMetric = ''.obs;
var selectedMetricId = 0.obs;
var localUnitsList = <String>[].obs;
var selectedUnit = ''.obs;
Future<void> fetchMetricsList() async {
try {
isMetricsLoading(true);
final response = await ApiBase.getRequest(
extendedURL: ApiUrl.fetchMasterTableUniList,
);
if (response.statusCode == 200) {
final MetricListSubmitModel metricsResponse =
metricListSubmitModelFromJson(response.body);
if (metricsResponse.isSuccess == true && metricsResponse.data != null) {
apiMetricsList.assignAll(metricsResponse.data!);
localMetricsList.assignAll(
metricsResponse.data!
.map((metric) => metric.metricName ?? '')
.toList(),
);
} else {
customSnackbar(
title: 'Error',
message: metricsResponse.message ?? 'Failed to fetch metrics',
duration: 2,
);
}
} else {
logger.warning('Failed to fetch metrics: HTTP ${response.statusCode}');
customSnackbar(
title: 'Error',
message: 'Failed to fetch metrics. Please try again.',
duration: 2,
);
}
} catch (e) {
logger.error('Error fetching metrics: $e');
customSnackbar(
title: 'Error',
message: 'Something went wrong. Please try again.',
duration: 2,
);
} finally {
isMetricsLoading(false);
}
}
void updateUnitsForSelectedMetric(List<String> units) {
selectedUnit.value = '';
localUnitsList.clear();
if (units.isNotEmpty) {
final uniqueUnits =
units.where((unit) => unit.isNotEmpty).toSet().toList();
localUnitsList.assignAll(uniqueUnits);
}
localUnitsList.refresh();
update();
}
//..........................................................................................
//..........................................................................................
//.............Connection Fetch (Exclusive).................................................
//..........................................................................................
//..........................................................................................
final RxList<Map<String, dynamic>> selectedConnections =
<Map<String, dynamic>>[].obs;
final RxList<String> selectedIds = <String>[].obs;
RxString connectionSearchValue = ''.obs;
RxInt connectionCurrentPage = 1.obs;
final int connectionPageSize = 10;
final hasMoreDataConnection = true.obs;
final isConnectionPaginationLoading = false.obs;
TextEditingController connectionSearchController = TextEditingController();
ExclusiveConnectionResponseModel exclusiveConnections =
ExclusiveConnectionResponseModel();
RxBool isConnectionsFetchLoading = false.obs;
Future<bool> fetchExclusiveConnections({
bool refresh = false,
int challengeId = 0,
}) async {
if (refresh) {
connectionCurrentPage.value = 1;
hasMoreDataConnection.value = true;
exclusiveConnections = ExclusiveConnectionResponseModel();
} else {
if (isConnectionPaginationLoading.value ||
hasMoreDataConnection.value == false) {
return false;
}
}
isConnectionPaginationLoading(true);
if (refresh) isConnectionsFetchLoading(true);
try {
final response = await ApiBase.getRequest(
extendedURL:
"${ApiUrl.fetchExclusiveConnectionsOfChallenge}/$challengeId?pageNumber=${connectionCurrentPage.value}&pagesize=$connectionPageSize&searchType=name&searchValue=${connectionSearchValue.value}",
);
if (response.statusCode == 200 || response.statusCode == 201) {
final newData = exclusiveConnectionResponseModelFromJson(response.body);
if (newData.isSuccess == true && newData.data != null) {
if (refresh) {
exclusiveConnections = newData;
} else {
// Append to existing items
final currentItems = exclusiveConnections.data?.items ?? [];
final newItems = newData.data?.items ?? [];
exclusiveConnections.data?.items = [...currentItems, ...newItems];
exclusiveConnections.data?.totalCount = newData.data?.totalCount;
}
final fetchedItemsCount =
exclusiveConnections.data?.items?.length ?? 0;
final totalItemsCount = newData.data?.totalCount ?? 0;
hasMoreDataConnection.value = fetchedItemsCount < totalItemsCount;
if (newData.data?.items?.isNotEmpty == true) {
connectionCurrentPage.value++;
} else {
// No more items to fetch
hasMoreDataConnection.value = false;
}
return true;
} else {
customSnackbar(
title: "Error",
message: "Failed to fetch connection details",
);
return false;
}
} else {
customSnackbar(
title: "Error",
message: "Failed to fetch connection details",
);
return false;
}
} catch (e) {
logger.error('Error fetching connections: $e');
return false;
} finally {
isConnectionsFetchLoading(false);
isConnectionPaginationLoading(false);
}
}
//..........................................................................................
void clearSearch() {
connectionSearchValue.value = '';
connectionCurrentPage.value = 1;
connectionSearchController.clear();
fetchExclusiveConnections(refresh: true);
}
//..........................................................................................
void exclusiveConnectionToggleSelection({
required int? tribeId,
required String? connectedUserId,
}) {
final String id =
tribeId != null ? 'tribe_$tribeId' : 'user_$connectedUserId';
if (selectedIds.contains(id)) {
// Unselect.....................
selectedIds.remove(id);
selectedConnections.removeWhere((element) {
return (tribeId != null && element['TribeID'] == tribeId) ||
(connectedUserId != null &&
element['ConnectedUserID'] == connectedUserId);
});
} else {
// Select........................
selectedIds.add(id);
selectedConnections.add({
"TribeID": tribeId,
"ConnectedUserID": connectedUserId,
});
}
}
}