import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/task_planing/report_task_action_controller.dart'; import 'package:marco/helpers/theme/app_theme.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/widgets/image_viewer_dialog.dart'; import 'package:marco/model/dailyTaskPlaning/create_task_botom_sheet.dart'; import 'dart:io'; class ReportActionBottomSheet extends StatefulWidget { final Map taskData; final VoidCallback? onCommentSuccess; final String taskDataId; final String workAreaId; final String activityId; final VoidCallback onReportSuccess; const ReportActionBottomSheet({ super.key, required this.taskData, this.onCommentSuccess, required this.taskDataId, required this.workAreaId, required this.activityId, required this.onReportSuccess, }); @override State createState() => _ReportActionBottomSheetState(); } class _Member { final String firstName; _Member(this.firstName); } class _ReportActionBottomSheetState extends State with UIMixin { late ReportTaskActionController controller; final ScrollController _scrollController = ScrollController(); String selectedAction = 'Select Action'; @override void initState() { super.initState(); controller = Get.put( ReportTaskActionController(), tag: widget.taskData['taskId'] ?? '', ); controller.fetchWorkStatuses(); controller.basicValidator.getController('approved_task')?.text = widget.taskData['approvedTask']?.toString() ?? ''; final data = widget.taskData; controller.basicValidator.getController('assigned_date')?.text = data['assignedOn'] ?? ''; controller.basicValidator.getController('assigned_by')?.text = data['assignedBy'] ?? ''; controller.basicValidator.getController('work_area')?.text = data['location'] ?? ''; controller.basicValidator.getController('activity')?.text = data['activity'] ?? ''; controller.basicValidator.getController('planned_work')?.text = data['plannedWork'] ?? ''; controller.basicValidator.getController('completed_work')?.text = data['completedWork'] ?? ''; controller.basicValidator.getController('team_members')?.text = (data['teamMembers'] as List).join(', '); controller.basicValidator.getController('assigned')?.text = data['assigned'] ?? ''; controller.basicValidator.getController('task_id')?.text = data['taskId'] ?? ''; controller.basicValidator.getController('comment')?.clear(); controller.basicValidator.getController('task_id')?.text = widget.taskDataId; controller.selectedImages.clear(); WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); } }); } String timeAgo(String dateString) { try { DateTime date = DateTime.parse(dateString + "Z").toLocal(); final now = DateTime.now(); final difference = now.difference(date); if (difference.inDays > 8) { return DateFormat('dd-MM-yyyy').format(date); } else if (difference.inDays >= 1) { return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago'; } else if (difference.inHours >= 1) { return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago'; } else if (difference.inMinutes >= 1) { return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago'; } else { return 'just now'; } } catch (e) { print('Error parsing date: $e'); return ''; } } @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom + 24, left: 24, right: 24, top: 12, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Drag handle Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.grey.shade400, borderRadius: BorderRadius.circular(2), ), ), GetBuilder( tag: widget.taskData['taskId'] ?? '', builder: (controller) { return Form( key: controller.basicValidator.formKey, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 4.0, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ MyText.titleMedium( "Take Report Action", fontWeight: 600, fontSize: 18, ), ], ), MySpacing.height(24), buildRow( "Assigned By", controller.basicValidator .getController('assigned_by') ?.text .trim(), icon: Icons.person_outline, ), buildRow( "Work Area", controller.basicValidator .getController('work_area') ?.text .trim(), icon: Icons.place_outlined, ), buildRow( "Activity", controller.basicValidator .getController('activity') ?.text .trim(), icon: Icons.assignment_outlined, ), buildRow( "Planned Work", controller.basicValidator .getController('planned_work') ?.text .trim(), icon: Icons.schedule_outlined, ), buildRow( "Completed Work", controller.basicValidator .getController('completed_work') ?.text .trim(), icon: Icons.done_all_outlined, ), buildTeamMembers(), MySpacing.height(8), Row( children: [ Icon(Icons.check_circle_outline, size: 18, color: Colors.grey[700]), MySpacing.width(8), MyText.titleSmall( "Approved Task:", fontWeight: 600, ), ], ), MySpacing.height(10), TextFormField( controller: controller.basicValidator .getController('approved_task'), keyboardType: TextInputType.number, validator: (value) { if (value == null || value.isEmpty) return 'Required'; if (int.tryParse(value) == null) return 'Must be a number'; return null; }, decoration: InputDecoration( hintText: "eg: 5", hintStyle: MyTextStyle.bodySmall(xMuted: true), border: outlineInputBorder, enabledBorder: outlineInputBorder, focusedBorder: focusedInputBorder, contentPadding: MySpacing.all(16), isCollapsed: true, floatingLabelBehavior: FloatingLabelBehavior.never, ), ), MySpacing.height(10), if ((widget.taskData['reportedPreSignedUrls'] as List?) ?.isNotEmpty == true) buildReportedImagesSection( imageUrls: List.from( widget.taskData['reportedPreSignedUrls'] ?? []), context: context, ), MySpacing.height(10), // Add this in your stateful widget MyText.titleSmall( "Report Actions", fontWeight: 600, ), MySpacing.height(10), Obx(() { final isLoading = controller.isLoadingWorkStatus.value; final workStatuses = controller.workStatus; if (isLoading) { return const Center( child: CircularProgressIndicator()); } return PopupMenuButton( onSelected: (String value) { controller.selectedWorkStatusName.value = value; controller.showAddTaskCheckbox.value = true; }, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), itemBuilder: (BuildContext context) { return workStatuses.map((status) { final statusName = status.name; return PopupMenuItem( value: statusName, child: Row( children: [ Radio( value: statusName, groupValue: controller .selectedWorkStatusName.value, onChanged: (_) => Navigator.pop(context, statusName), ), const SizedBox(width: 8), MyText.bodySmall(statusName), ], ), ); }).toList(); }, child: Container( padding: MySpacing.xy(16, 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular( AppStyle.buttonRadius.medium), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.bodySmall( controller.selectedWorkStatusName.value .isEmpty ? "Select Work Status" : controller .selectedWorkStatusName.value, color: Colors.black87, ), const Icon(Icons.arrow_drop_down, size: 20), ], ), ), ); }), MySpacing.height(10), Obx(() { if (!controller.showAddTaskCheckbox.value) return SizedBox.shrink(); final checkboxTheme = CheckboxThemeData( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(2)), side: WidgetStateBorderSide.resolveWith((states) => BorderSide( color: states.contains(WidgetState.selected) ? Colors.transparent : Colors.black)), fillColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) ? Colors.blueAccent : Colors.white), checkColor: WidgetStateProperty.all(Colors.white), ); return Theme( data: Theme.of(context) .copyWith(checkboxTheme: checkboxTheme), child: CheckboxListTile( title: MyText.titleSmall( "Add new task", fontWeight: 600, ), value: controller.isAddTaskChecked.value, onChanged: (val) => controller .isAddTaskChecked.value = val ?? false, controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, ), ); }), MySpacing.height(10), Row( children: [ Icon(Icons.comment_outlined, size: 18, color: Colors.grey[700]), MySpacing.width(8), MyText.titleSmall( "Comment:", fontWeight: 600, ), ], ), MySpacing.height(8), TextFormField( validator: controller.basicValidator .getValidation('comment'), controller: controller.basicValidator .getController('comment'), keyboardType: TextInputType.text, decoration: InputDecoration( hintText: "eg: Work done successfully", hintStyle: MyTextStyle.bodySmall(xMuted: true), border: outlineInputBorder, enabledBorder: outlineInputBorder, focusedBorder: focusedInputBorder, contentPadding: MySpacing.all(16), isCollapsed: true, floatingLabelBehavior: FloatingLabelBehavior.never, ), ), MySpacing.height(16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.camera_alt_outlined, size: 18, color: Colors.grey[700]), MySpacing.width(8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleSmall("Attach Photos:", fontWeight: 600), MySpacing.height(12), ], ), ), ], ), Obx(() { final images = controller.selectedImages; return buildImagePickerSection( images: images, onCameraTap: () => controller.pickImages(fromCamera: true), onUploadTap: () => controller.pickImages(fromCamera: false), onRemoveImage: (index) => controller.removeImageAt(index), onPreviewImage: (index) { showDialog( context: context, builder: (_) => ImageViewerDialog( imageSources: images, initialIndex: index, ), ); }, ); }), MySpacing.height(24), buildCommentActionButtons( onCancel: () => Navigator.of(context).pop(), onSubmit: () async { if (controller.basicValidator.validateForm()) { final selectedStatusName = controller.selectedWorkStatusName.value; final selectedStatus = controller.workStatus.firstWhereOrNull( (status) => status.name == selectedStatusName, ); final reportActionId = selectedStatus?.id.toString() ?? ''; final approvedTaskCount = controller .basicValidator .getController('approved_task') ?.text .trim() ?? ''; final shouldShowAddTaskSheet = controller.isAddTaskChecked.value; final success = await controller.approveTask( projectId: controller.basicValidator .getController('task_id') ?.text ?? '', comment: controller.basicValidator .getController('comment') ?.text ?? '', images: controller.selectedImages, reportActionId: reportActionId, approvedTaskCount: approvedTaskCount, ); if (success) { Navigator.of(context).pop(); if (shouldShowAddTaskSheet) { await Future.delayed( Duration(milliseconds: 100)); showCreateTaskBottomSheet( workArea: widget.taskData['location'] ?? '', activity: widget.taskData['activity'] ?? '', completedWork: widget.taskData['completedWork'] ?? '', unit: widget.taskData['unit'] ?? '', onCategoryChanged: (category) { debugPrint( "Category changed to: $category"); }, parentTaskId: widget.taskDataId, plannedTask: int.tryParse( widget.taskData['plannedWork'] ?? '0') ?? 0, activityId: widget.activityId, workAreaId: widget.workAreaId, onSubmit: () { Navigator.of(context).pop(); }, ); } widget.onReportSuccess.call(); } } }, isLoading: controller.isLoading, ), MySpacing.height(20), if ((widget.taskData['taskComments'] as List?) ?.isNotEmpty == true) ...[ Row( children: [ MySpacing.width(10), Icon(Icons.chat_bubble_outline, size: 18, color: Colors.grey[700]), MySpacing.width(8), MyText.titleSmall( "Comments", fontWeight: 600, ), ], ), MySpacing.height(12), Builder( builder: (context) { final comments = List>.from( widget.taskData['taskComments'] as List, ); return buildCommentList(comments, context); }, ) ], ], ), ), ); }, ), ], ), ), ); } Widget buildReportedImagesSection({ required List imageUrls, required BuildContext context, String title = "Reported Images", }) { if (imageUrls.isEmpty) return const SizedBox(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(8), Padding( padding: const EdgeInsets.symmetric(horizontal: 0.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.image_outlined, size: 18, color: Colors.grey[700]), MySpacing.width(8), MyText.titleSmall( title, fontWeight: 600, ), ], ), ), MySpacing.height(8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( height: 70, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: imageUrls.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final url = imageUrls[index]; return GestureDetector( onTap: () { showDialog( context: context, barrierColor: Colors.black54, builder: (_) => ImageViewerDialog( imageSources: imageUrls, initialIndex: index, ), ); }, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( url, width: 70, height: 70, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( width: 70, height: 70, color: Colors.grey.shade200, child: Icon(Icons.broken_image, color: Colors.grey[600]), ), ), ), ); }, ), ), ), MySpacing.height(16), ], ); } Widget buildTeamMembers() { final teamMembersText = controller.basicValidator.getController('team_members')?.text ?? ''; final members = teamMembersText .split(',') .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); return Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ MyText.titleSmall( "Team Members:", fontWeight: 600, ), MySpacing.width(12), GestureDetector( onTap: () { TeamBottomSheet.show( context: context, teamMembers: members.map((name) => _Member(name)).toList(), ); }, child: SizedBox( height: 32, width: 100, child: Stack( children: [ for (int i = 0; i < members.length.clamp(0, 3); i++) Positioned( left: i * 24.0, child: Tooltip( message: members[i], child: Avatar( firstName: members[i], lastName: '', size: 32, ), ), ), if (members.length > 3) Positioned( left: 2 * 24.0, child: CircleAvatar( radius: 16, backgroundColor: Colors.grey.shade300, child: MyText.bodyMedium( '+${members.length - 3}', style: const TextStyle( fontSize: 12, color: Colors.black87), ), ), ), ], ), ), ), ], ), ); } Widget buildCommentActionButtons({ required VoidCallback onCancel, required Future Function() onSubmit, required RxBool isLoading, double? buttonHeight, }) { return Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: onCancel, icon: const Icon(Icons.close, color: Colors.red, size: 18), label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.red), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), ), ), const SizedBox(width: 16), Expanded( child: Obx(() { return ElevatedButton.icon( onPressed: isLoading.value ? null : () => onSubmit(), icon: isLoading.value ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Icon(Icons.send, color: Colors.white, size: 18), label: isLoading.value ? const SizedBox() : MyText.bodyMedium("Submit", color: Colors.white, fontWeight: 600), style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), ); }), ), ], ); } Widget buildRow(String label, String? value, {IconData? icon}) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (icon != null) Padding( padding: const EdgeInsets.only(right: 8.0, top: 2), child: Icon(icon, size: 18, color: Colors.grey[700]), ), MyText.titleSmall( "$label:", fontWeight: 600, ), MySpacing.width(12), Expanded( child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"), ), ], ), ); } Widget buildCommentList( List> comments, BuildContext context) { comments.sort((a, b) { final aDate = DateTime.tryParse(a['date'] ?? '') ?? DateTime.fromMillisecondsSinceEpoch(0); final bDate = DateTime.tryParse(b['date'] ?? '') ?? DateTime.fromMillisecondsSinceEpoch(0); return bDate.compareTo(aDate); // newest first }); return SizedBox( height: 300, child: ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: comments.length, itemBuilder: (context, index) { final comment = comments[index]; final commentText = comment['text'] ?? '-'; final commentedBy = comment['commentedBy'] ?? 'Unknown'; final relativeTime = timeAgo(comment['date'] ?? ''); final imageUrls = List.from(comment['preSignedUrls'] ?? []); return Container( margin: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Avatar( firstName: commentedBy.split(' ').first, lastName: commentedBy.split(' ').length > 1 ? commentedBy.split(' ').last : '', size: 32, ), const SizedBox(width: 12), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.bodyMedium( commentedBy, fontWeight: 700, color: Colors.black87, ), MyText.bodySmall( relativeTime, fontSize: 12, color: Colors.black54, ), ], ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: MyText.bodyMedium( commentText, fontWeight: 500, color: Colors.black87, maxLines: null, ), ), ], ), const SizedBox(height: 12), if (imageUrls.isNotEmpty) ...[ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.attach_file_outlined, size: 18, color: Colors.grey[700]), MyText.bodyMedium( 'Attachments', fontWeight: 600, color: Colors.black87, ), ], ), const SizedBox(height: 8), SizedBox( height: 60, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: imageUrls.length, itemBuilder: (context, imageIndex) { final imageUrl = imageUrls[imageIndex]; return GestureDetector( onTap: () { showDialog( context: context, barrierColor: Colors.black54, builder: (_) => ImageViewerDialog( imageSources: imageUrls, initialIndex: imageIndex, ), ); }, child: Stack( children: [ Container( width: 60, height: 60, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.grey[100], boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 6, offset: Offset(2, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( imageUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( color: Colors.grey[300], child: Icon(Icons.broken_image, color: Colors.grey[700]), ), ), ), ), const Positioned( right: 4, bottom: 4, child: Icon(Icons.zoom_in, color: Colors.white70, size: 16), ), ], ), ); }, separatorBuilder: (_, __) => const SizedBox(width: 12), ), ), const SizedBox(height: 12), ], ], ), ), ], ), ); }, ), ); } Widget buildImagePickerSection({ required List images, required VoidCallback onCameraTap, required VoidCallback onUploadTap, required void Function(int index) onRemoveImage, required void Function(int initialIndex) onPreviewImage, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (images.isEmpty) Container( height: 70, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300, width: 2), color: Colors.grey.shade100, ), child: Center( child: Icon(Icons.photo_camera_outlined, size: 48, color: Colors.grey.shade400), ), ) else SizedBox( height: 70, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: images.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final file = images[index]; return Stack( children: [ GestureDetector( onTap: () => onPreviewImage(index), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( file, height: 70, width: 70, fit: BoxFit.cover, ), ), ), Positioned( top: 4, right: 4, child: GestureDetector( onTap: () => onRemoveImage(index), child: Container( decoration: const BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 20, color: Colors.white), ), ), ), ], ); }, ), ), MySpacing.height(16), Row( children: [ Expanded( child: MyButton.outlined( onPressed: onCameraTap, padding: MySpacing.xy(12, 10), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.camera_alt, size: 16, color: Colors.blueAccent), MySpacing.width(6), MyText.bodySmall('Capture', color: Colors.blueAccent), ], ), ), ), MySpacing.width(12), Expanded( child: MyButton.outlined( onPressed: onUploadTap, padding: MySpacing.xy(12, 10), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.upload_file, size: 16, color: Colors.blueAccent), MySpacing.width(6), MyText.bodySmall('Upload', color: Colors.blueAccent), ], ), ), ), ], ), ], ); } }