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 startDate = Rx(null); Rx endDate = Rx(null); // File picker observables RxString demoVideoName = ''.obs; RxString bannerImageName = ''.obs; // File objects to store the actual files Rx demoVideoFile = Rx(null); Rx bannerImageFile = Rx(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 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 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 updateChallengePrivacy({ required int selectedChallengeId, }) async { bool ret = false; isUpdatePrivacyLoading.value = true; try { // Prepare form data Map 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 createChallenge() async { bool ret = false; if (!validateForm()) return false; isCreatingChallengeLoading.value = true; try { // Prepare form data (body fields) Map 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> 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 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 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 = [].obs; var selectedFitnessGoal = "".obs; RxList localFitnessGoalsList = [].obs; RxInt selectedFitnessGoalId = 0.obs; Future 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 = [].obs; var localMetricsList = [].obs; var selectedMetric = ''.obs; var selectedMetricId = 0.obs; var localUnitsList = [].obs; var selectedUnit = ''.obs; Future 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 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> selectedConnections = >[].obs; final RxList selectedIds = [].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 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, }); } } }