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

836 lines
32 KiB
Dart

import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:onufitness/constants/api_enum_constant.dart';
import 'package:onufitness/constants/color_constant.dart';
import 'package:onufitness/constants/text_constant.dart';
import 'package:onufitness/screens/rise/controllers/create_challenge_controller.dart';
import 'package:onufitness/screens/rise/controllers/rise_controller.dart';
import 'package:onufitness/screens/echoboard/widget/exclusive_connection_tile.dart';
import 'package:onufitness/services/local_storage_services/shared_services.dart';
import 'package:onufitness/utils/custom_sneakbar.dart';
import 'package:onufitness/widgets/Dropdowns/custom_lebel_dropdown.dart';
import 'package:onufitness/widgets/appbars/custom_appbar.dart';
import 'package:onufitness/widgets/TextFields/custom_textfield_with_inside_container_label_text.dart';
import 'package:onufitness/widgets/bottomsheet/common_upload_option_bottomsheet.dart';
import 'package:onufitness/widgets/pickers/lebel_text_date_picker.dart';
import 'package:onufitness/widgets/pickers/lebel_text_file_picker.dart';
class CreateChallengeScreen extends StatefulWidget {
const CreateChallengeScreen({super.key});
@override
State<CreateChallengeScreen> createState() => _CreateChallengeScreenState();
}
class _CreateChallengeScreenState extends State<CreateChallengeScreen> {
final ScrollController _membersScrollController = ScrollController();
CreateChallengeController createChallengeController = Get.put(
CreateChallengeController(),
);
final riseController = Get.find<RiseController>();
@override
void initState() {
super.initState();
_membersScrollController.addListener(_onMembersScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
createChallengeController.fetchExclusiveConnections(refresh: true);
createChallengeController.fetchMetricsList();
createChallengeController.fetchFitnessGoals();
});
}
void _onMembersScroll() {
if (_membersScrollController.position.pixels >=
_membersScrollController.position.maxScrollExtent - 200) {
if (createChallengeController.hasMoreDataConnection.value &&
!createChallengeController.isConnectionPaginationLoading.value) {
createChallengeController.fetchExclusiveConnections();
}
}
}
Future<void> _onRefresh() async {
await createChallengeController.fetchExclusiveConnections(refresh: true);
createChallengeController.selectedConnections.clear();
createChallengeController.selectedConnections.refresh();
createChallengeController.selectedIds.clear();
return Future.value();
}
@override
void dispose() {
createChallengeController.clearAll();
createChallengeController.selectedConnections.clear();
createChallengeController.selectedConnections.refresh();
createChallengeController.selectedIds.clear();
super.dispose();
}
bool hasValidWordLength(String text) {
final wordCount = text.trim().split(RegExp(r'\s+')).length;
return wordCount <= 100;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: CustomAppBar(
title: 'Create a Challenge',
textColor: appbarTextColor,
titleFontSize: appBarHeardingText,
backgroundColor: Colors.white,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios, color: Colors.black),
onPressed: () => Get.back(),
),
),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 10.h),
StatefulBuilder(
builder: (context, setState) {
return CustomTextFieldContainerInsideLabel(
controller: createChallengeController.titleController,
borderColor: lightGreyColor,
label: 'Challenge Title *',
fillColor: textFieldFillColor,
lebelTextPadding: 12.h,
topContentPadding: 20.h,
height:
createChallengeController.titleController.text.isEmpty
? 40.h
: null,
lebelTextFontWeight: FontWeight.w500,
);
},
),
SizedBox(height: 15),
Obx(() {
return createChallengeController.isFitnessGoalsLoading.value
? const Center(child: CircularProgressIndicator())
: EnhancedLebelTextDropdown(
title: "Fitness Goals *",
items: createChallengeController.localFitnessGoalsList,
selectedItem:
createChallengeController.selectedFitnessGoal.isEmpty
? null
: createChallengeController
.selectedFitnessGoal
.value,
onChanged: (String? newValue) {
if (newValue != null) {
createChallengeController.selectedFitnessGoal.value =
newValue;
var selectedObj = createChallengeController
.apiFitnessGoalsList
.firstWhere(
(element) => element.fitnessGoalTitle == newValue,
);
createChallengeController.selectedFitnessGoalId.value =
selectedObj.fitnessGoalId!;
}
},
borderColor: lightGreyColor,
);
}),
SizedBox(height: 15.h),
Obx(
() => EnhancedDatePicker(
label: "Start date *",
selectedDate:
createChallengeController.startDate.value?.toString() ?? "",
onDatePicked: createChallengeController.pickStartDate,
borderColor: lightGreyColor,
),
),
SizedBox(height: 15.h),
Obx(
() => EnhancedDatePicker(
label: "End date *",
selectedDate:
createChallengeController.endDate.value?.toString() ?? "",
onDatePicked: createChallengeController.pickEndDate,
borderColor: lightGreyColor,
),
),
SizedBox(height: 15.h),
CustomTextFieldContainerInsideLabel(
borderColor: lightGreyColor,
label: 'Description *',
fillColor: textFieldFillColor,
controller: createChallengeController.descriptionController,
maxLine: 4,
lebelTextPadding: 12.h,
topContentPadding: 30.h,
lebelTextFontWeight: FontWeight.w500,
),
SizedBox(height: 15.h),
// Upload Demo Video
Obx(
() => EnhancedFilePickerTile(
hintText: "Upload Demo Video",
fileName:
createChallengeController.demoVideoName.value.isEmpty
? null
: createChallengeController.demoVideoName.value,
icon: Icons.file_upload_outlined,
onTap: () {
CommonUploadBottomSheet.show(
context: context,
title: "Upload Video From",
allowedFileTypes: ['mp4', 'm4v'],
maxFileSizeMB: 50.0,
showCamera: false,
showGallery: true,
showDocument: true,
onFileSelected: (
String filePath,
String fileName,
String fileExtension,
) {
// Convert String path to PlatformFile
createChallengeController
.demoVideoFile
.value = PlatformFile(
path: filePath,
name: fileName,
size: File(filePath).lengthSync(),
bytes: null,
);
createChallengeController.demoVideoName.value = fileName;
},
onError: (String errorMessage) {
// Show error message
customSnackbar(title: "Error", message: errorMessage);
},
);
},
onClear: () => createChallengeController.clearDemoVideo(),
),
),
SizedBox(height: 16.h),
// Upload Banner Image
Obx(
() => EnhancedFilePickerTile(
hintText: "Upload Banner Image",
fileName:
createChallengeController.bannerImageName.value.isEmpty
? null
: createChallengeController.bannerImageName.value,
icon: Icons.image_outlined,
onTap: () {
CommonUploadBottomSheet.show(
context: context,
title: "Upload Image from",
allowedFileTypes: ['jpg', 'jpeg', 'png'],
maxFileSizeMB: 5.0,
showCamera: true,
showGallery: true,
showDocument: true,
onFileSelected: (
String filePath,
String fileName,
String fileExtension,
) {
// Convert String path to PlatformFile
createChallengeController
.bannerImageFile
.value = PlatformFile(
path: filePath,
name: fileName,
size: File(filePath).lengthSync(),
bytes: null,
);
createChallengeController.bannerImageName.value =
fileName;
},
onError: (String errorMessage) {
// Show error message
customSnackbar(title: "Error", message: errorMessage);
},
);
},
onClear: () => createChallengeController.clearBannerImage(),
),
),
SizedBox(height: 16.h),
Container(
width: double.infinity,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
color: Colors.grey[100],
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Challenge task",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 12.h),
SizedBox(height: 12.h),
CustomTextFieldContainerInsideLabel(
controller: createChallengeController.taskTitleController,
borderColor: lightGreyColor,
label: "Title *",
fillColor: Colors.white,
lebelTextPadding: 12.h,
topContentPadding: 20.h,
),
SizedBox(height: 12.h),
// Updated Metric Dropdown
Obx(() {
return createChallengeController.isMetricsLoading.value
? const Center(child: CircularProgressIndicator())
: EnhancedLebelTextDropdown(
title: "Metric *",
items: createChallengeController.localMetricsList,
selectedItem:
createChallengeController.selectedMetric.isEmpty
? null
: createChallengeController
.selectedMetric
.value,
onChanged: (String? newValue) {
if (newValue != null) {
createChallengeController.selectedMetric.value =
newValue;
// Find the selected metric object to get its ID and units
var selectedMetricObj = createChallengeController
.apiMetricsList
.firstWhere(
(element) => element.metricName == newValue,
);
createChallengeController.selectedMetricId.value =
selectedMetricObj.metricId!;
// Update units list based on selected metric
createChallengeController
.updateUnitsForSelectedMetric(
selectedMetricObj.metricUnits ?? [],
);
// Clear selected unit when metric changes
createChallengeController.selectedUnit.value = '';
}
},
borderColor: lightGreyColor,
filledColor: Colors.white,
);
}),
SizedBox(height: 12.h),
CustomTextFieldContainerInsideLabel(
controller: createChallengeController.targetValueController,
borderColor: lightGreyColor,
label: 'Target Value *',
fillColor: Colors.white,
lebelTextPadding: 12.h,
keyboardType: TextInputType.numberWithOptions(),
topContentPadding: 20.h,
),
SizedBox(height: 12.h),
// Updated Unit Dropdown
Obx(() {
return EnhancedLebelTextDropdown(
title: "Unit *",
items: createChallengeController.localUnitsList.toList(),
selectedItem:
createChallengeController.selectedUnit.isEmpty
? null
: createChallengeController.selectedUnit.value,
onChanged: (String? newValue) {
if (newValue != null) {
createChallengeController.selectedUnit.value =
newValue;
}
},
borderColor: lightGreyColor,
filledColor: Colors.white,
);
}),
],
),
),
SizedBox(height: 24.h),
// Challenge Visibility Section
challengeVisibilitySection(),
SizedBox(height: 24.h),
Obx(() {
if (createChallengeController.challengeVisibilityId.value == 3) {
return Column(
children: [membersSelectionSection(), SizedBox(height: 32.h)],
);
}
return SizedBox(height: 32.h);
}),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
padding: EdgeInsets.symmetric(vertical: 14.h),
),
child: Text(
'Cancel',
style: TextStyle(
fontSize: regularSizeText,
fontWeight: FontWeight.w600,
),
),
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(
() => ElevatedButton(
onPressed: () async {
if (!createChallengeController
.isCreatingChallengeLoading
.value) {
await createChallengeController
.createChallenge()
.then((value) async {
if (value) {
await riseController.fetchOngoingChallenges(
isRefresh: true,
);
await riseController.fetchUpcomingChallenges(
isRefresh: true,
);
await riseController.fetchCreatedChallenges(
isRefresh: true,
);
await riseController.fetchJoinedChallenges(
isRefresh: true,
);
}
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor:
createChallengeController
.isCreatingChallengeLoading
.value
? lightGreyColor
: Color(primaryColor),
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
padding: EdgeInsets.symmetric(vertical: 14.h),
),
child:
createChallengeController
.isCreatingChallengeLoading
.value
? SizedBox(
height: 20.h,
width: 20.w,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.0,
),
)
: Text(
'Create',
style: TextStyle(
fontSize: regularSizeText,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
SizedBox(height: 60.h),
],
),
),
);
}
Widget challengeVisibilitySection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (SharedServices.getLoginDetails()?.data?.userRole ==
ApiEnum.coachUserRole)
Obx(
() => Row(
children: [
Checkbox(
value: createChallengeController.isAutoJoin.value,
onChanged: (bool? value) {
createChallengeController.isAutoJoin.value = value ?? true;
},
activeColor: Color(primaryColor),
checkColor: Colors.white,
),
Expanded(
child: Text(
'Auto-join challenge (You will automatically join this challenge)',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
],
),
),
if (SharedServices.getLoginDetails()?.data?.userRole ==
ApiEnum.coachUserRole)
SizedBox(height: 15.h),
Text(
'Challenge Visibility',
style: TextStyle(
fontSize: regularSizeText,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
SizedBox(height: 10.h),
Obx(() {
return Column(
children: [
RadioListTile<int>(
title: Text(
'Public',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'Anyone can join this challenge',
style: TextStyle(fontSize: 13.sp, color: greyTextColor1),
),
value: ApiEnum.publicPostVisibilityID,
groupValue:
createChallengeController.challengeVisibilityId.value,
activeColor: Color(primaryColor),
onChanged: (int? value) {
if (value != null) {
createChallengeController.challengeVisibilityId.value =
value;
// Clear selected connections when switching to public
if (value == 1) {
createChallengeController.selectedConnections.clear();
createChallengeController.selectedIds.clear();
}
}
},
contentPadding: EdgeInsets.zero,
),
RadioListTile<int>(
title: Text(
'Exclusive',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'Only selected members can join',
style: TextStyle(fontSize: 13.sp, color: greyTextColor1),
),
value: ApiEnum.exclusivePostVisibilityID,
groupValue:
createChallengeController.challengeVisibilityId.value,
activeColor: Color(primaryColor),
onChanged: (int? value) {
if (value != null) {
createChallengeController.challengeVisibilityId.value =
value;
}
},
contentPadding: EdgeInsets.zero,
),
],
);
}),
],
);
}
Widget membersSelectionSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Members and Groups',
style: TextStyle(
fontSize: regularSizeText,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
SizedBox(height: 5.h),
Text(
'Select members and groups to add to your challenge.',
style: TextStyle(fontSize: verySmallSizeText, color: greyTextColor1),
),
SizedBox(height: 20.h),
// Search Field
TextField(
controller: createChallengeController.connectionSearchController,
decoration: InputDecoration(
hintText: 'Search for members',
hintStyle: TextStyle(
fontWeight: FontWeight.w600,
color: greyBorderColor,
),
prefixIcon: Padding(
padding: EdgeInsets.only(left: 20.w),
child: Icon(Icons.search),
),
suffixIcon: IconButton(
icon: Padding(
padding: EdgeInsets.only(right: 10.w),
child: Icon(Icons.close),
),
onPressed: () => createChallengeController.clearSearch(),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(color: lightGreyColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(color: lightGreyColor),
),
filled: true,
fillColor: textFieldFillColor,
),
onChanged: (newValue) {
createChallengeController.connectionSearchValue.value = newValue;
},
onSubmitted: (_) {
createChallengeController.fetchExclusiveConnections(refresh: true);
},
),
SizedBox(height: 20.h),
Container(
height: 400.h,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8.r),
),
child: Obx(
() =>
createChallengeController.isConnectionsFetchLoading.value &&
createChallengeController
.exclusiveConnections
.data
?.items ==
null
? Center(
child: CircularProgressIndicator(color: Colors.black),
)
: createChallengeController
.exclusiveConnections
.data
?.items ==
null ||
createChallengeController
.exclusiveConnections
.data!
.items!
.isEmpty
? Center(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView(
physics: AlwaysScrollableScrollPhysics(),
children: [
SizedBox(height: 150.h),
Center(child: Text('No members found')),
],
),
),
)
: RefreshIndicator(
backgroundColor: Colors.white,
color: Colors.black,
onRefresh: _onRefresh,
child: ListView.builder(
controller: _membersScrollController,
physics: AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.all(8.w),
itemCount:
createChallengeController
.exclusiveConnections
.data!
.items!
.length +
(createChallengeController
.isConnectionPaginationLoading
.value
? 1
: 0),
itemBuilder: (_, i) {
if (i ==
createChallengeController
.exclusiveConnections
.data!
.items!
.length) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: CircularProgressIndicator(
backgroundColor: Colors.white,
color: Colors.black,
),
),
);
}
final connection =
createChallengeController
.exclusiveConnections
.data!
.items![i];
final String itemId =
connection.tribeId != null
? 'tribe_${connection.tribeId}'
: 'user_${connection.connectedUserId}';
return Obx(() {
final bool isSelected = createChallengeController
.selectedIds
.contains(itemId);
return ExclusiveConnectionTile(
name:
connection.tribeId != null
? connection.tribeName ?? 'Unknown Tribe'
: connection.fullName ?? 'Unknown User',
type:
connection.tribeId != null ? 'group' : 'user',
members: connection.totalMemberCount,
imageUrl:
connection.tribeId != null
? connection.adminProfilePicture ?? ''
: connection.profilePicture ?? '',
isSelected: isSelected,
onTap: () {
createChallengeController
.exclusiveConnectionToggleSelection(
tribeId: connection.tribeId,
connectedUserId:
connection.connectedUserId,
);
},
);
});
},
),
),
),
),
],
);
}
}
Widget filePickerTile({
required String label,
String? fileName,
required IconData icon,
required VoidCallback onTap,
VoidCallback? onClear,
bool isRequired = false,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10.r),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
decoration: BoxDecoration(
color: textFieldFillColor,
borderRadius: BorderRadius.circular(10.r),
border: Border.all(color: lightGreyColor),
),
child: Row(
children: [
Icon(icon, color: Colors.grey),
SizedBox(width: 12.w),
Expanded(
child: Text(
fileName ?? "$label${isRequired ? ' *' : ''}",
style: TextStyle(
fontSize: 15.sp,
color: fileName == null ? Colors.black54 : Colors.black,
fontWeight:
fileName == null ? FontWeight.normal : FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
if (fileName != null && fileName.isNotEmpty && onClear != null)
GestureDetector(
onTap: () {
onClear();
},
child: Padding(
padding: EdgeInsets.only(left: 8.w),
child: Icon(Icons.close, color: Colors.grey),
),
),
],
),
),
);
}