import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'dart:io'; import 'dart:math' as math; // --- Assumed Imports (ensure these paths are correct in your project) --- import 'package:marco/controller/task_planning/report_task_controller.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:marco/helpers/widgets/image_viewer_dialog.dart'; import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; // --- Form Field Keys (Unchanged) --- class _FormFieldKeys { static const String assignedDate = 'assigned_date'; static const String assignedBy = 'assigned_by'; static const String workArea = 'work_area'; static const String activity = 'activity'; static const String plannedWork = 'planned_work'; static const String completedWork = 'completed_work'; static const String teamMembers = 'team_members'; static const String assigned = 'assigned'; static const String taskId = 'task_id'; static const String comment = 'comment'; } // --- Main Widget: CommentTaskBottomSheet --- class CommentTaskBottomSheet extends StatefulWidget { final Map taskData; final VoidCallback? onCommentSuccess; final String taskDataId; final String workAreaId; final String activityId; const CommentTaskBottomSheet({ super.key, required this.taskData, this.onCommentSuccess, required this.taskDataId, required this.workAreaId, required this.activityId, }); @override State createState() => _CommentTaskBottomSheetState(); } class _Member { final String firstName; _Member(this.firstName); } class _CommentTaskBottomSheetState extends State with UIMixin { late final ReportTaskController controller; List> _sortedComments = []; @override void initState() { super.initState(); controller = Get.put(ReportTaskController(), tag: widget.taskData['taskId'] ?? UniqueKey().toString()); _initializeControllerData(); final comments = List>.from( widget.taskData['taskComments'] as List? ?? []); 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 }); _sortedComments = comments; } void _initializeControllerData() { final data = widget.taskData; final fieldMappings = { _FormFieldKeys.assignedDate: data['assignedOn'], _FormFieldKeys.assignedBy: data['assignedBy'], _FormFieldKeys.workArea: data['location'], _FormFieldKeys.activity: data['activity'], _FormFieldKeys.plannedWork: data['plannedWork'], _FormFieldKeys.completedWork: data['completedWork'], _FormFieldKeys.teamMembers: (data['teamMembers'] as List?)?.join(', '), _FormFieldKeys.assigned: data['assigned'], _FormFieldKeys.taskId: data['taskId'], }; for (final entry in fieldMappings.entries) { controller.basicValidator.getController(entry.key)?.text = entry.value ?? ''; } controller.basicValidator.getController(_FormFieldKeys.comment)?.clear(); controller.selectedImages.clear(); } String _timeAgo(String dateString) { // This logic remains unchanged try { final date = DateTime.parse(dateString + "Z").toLocal(); final difference = DateTime.now().difference(date); if (difference.inDays > 8) return DateFormat('dd-MM-yyyy').format(date); if (difference.inDays >= 1) return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago'; if (difference.inHours >= 1) return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago'; if (difference.inMinutes >= 1) return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago'; return 'just now'; } catch (e) { debugPrint('Error parsing date for timeAgo: $e'); return dateString; } } @override Widget build(BuildContext context) { // --- REFACTORING POINT --- // The entire widget now returns a BaseBottomSheet, passing the content as its child. // The GetBuilder provides reactive state (like isLoading) to the BaseBottomSheet. return GetBuilder( tag: widget.taskData['taskId'] ?? '', builder: (controller) { return BaseBottomSheet( title: "Task Details & Comments", onCancel: () => Navigator.of(context).pop(), onSubmit: _submitComment, isSubmitting: controller.isLoading.value, bottomContent: _buildCommentsSection(), child: Form( // moved to last key: controller.basicValidator.formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeaderActions(), MySpacing.height(12), _buildTaskDetails(), _buildReportedImages(), _buildCommentInput(), _buildImagePicker(), ], ), ), ); }, ); } // --- REFACTORING POINT --- // The original _buildHeader is now split. The title is handled by BaseBottomSheet. // This new widget contains the remaining actions from the header. Widget _buildHeaderActions() { return Align( alignment: Alignment.centerRight, child: InkWell( onTap: () => _showCreateTaskBottomSheet(), borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.1), borderRadius: BorderRadius.circular(16), ), child: MyText.bodySmall( "+ Create Task", fontWeight: 600, color: Colors.blueAccent, ), ), ), ); } Widget _buildTaskDetails() { return Column( children: [ _buildDetailRow( "Assigned By", controller.basicValidator .getController(_FormFieldKeys.assignedBy) ?.text, icon: Icons.person_outline), _buildDetailRow( "Work Area", controller.basicValidator .getController(_FormFieldKeys.workArea) ?.text, icon: Icons.place_outlined), _buildDetailRow( "Activity", controller.basicValidator .getController(_FormFieldKeys.activity) ?.text, icon: Icons.assignment_outlined), _buildDetailRow( "Planned Work", controller.basicValidator .getController(_FormFieldKeys.plannedWork) ?.text, icon: Icons.schedule_outlined), _buildDetailRow( "Completed Work", controller.basicValidator .getController(_FormFieldKeys.completedWork) ?.text, icon: Icons.done_all_outlined), _buildTeamMembers(), ], ); } Widget _buildReportedImages() { final imageUrls = List.from(widget.taskData['reportedPreSignedUrls'] ?? []); if (imageUrls.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(bottom: 8.0), child: _buildSectionHeader("Reported Images", Icons.image_outlined), ), // --- Refactoring Note --- // Using the reusable _ImageHorizontalListView widget. _ImageHorizontalListView( imageSources: imageUrls, onPreview: (index) => _showImageViewer(imageUrls, index), ), MySpacing.height(16), ], ); } Widget _buildCommentInput() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader("Add Note", Icons.comment_outlined), MySpacing.height(8), TextFormField( validator: controller.basicValidator.getValidation(_FormFieldKeys.comment), controller: controller.basicValidator.getController(_FormFieldKeys.comment), keyboardType: TextInputType.multiline, maxLines: null, // Allows for multiline input 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), ], ); } Widget _buildImagePicker() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader("Attach Photos", Icons.camera_alt_outlined), MySpacing.height(12), Obx(() { final images = controller.selectedImages; return Column( children: [ // --- Refactoring Note --- // Using the reusable _ImageHorizontalListView for picked images. _ImageHorizontalListView( imageSources: images.toList(), onPreview: (index) => _showImageViewer(images.toList(), index), onRemove: (index) => controller.removeImageAt(index), emptyStatePlaceholder: Container( height: 70, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300, width: 1.5), color: Colors.grey.shade100, ), child: Center( child: Icon(Icons.photo_library_outlined, size: 36, color: Colors.grey.shade400), ), ), ), MySpacing.height(16), Row( children: [ _buildPickerButton( onTap: () => controller.pickImages(fromCamera: true), icon: Icons.camera_alt, label: 'Capture', ), MySpacing.width(12), _buildPickerButton( onTap: () => controller.pickImages(fromCamera: false), icon: Icons.upload_file, label: 'Upload', ), ], ), ], ); }), ], ); } Widget _buildCommentsSection() { if (_sortedComments.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(24), _buildSectionHeader("Comments", Icons.chat_bubble_outline), MySpacing.height(12), // --- Refactoring Note --- // Using a ListView instead of a fixed-height SizedBox for better responsiveness. // It's constrained by the parent SingleChildScrollView. ListView.builder( shrinkWrap: true, // Important for ListView inside SingleChildScrollView physics: const NeverScrollableScrollPhysics(), // Parent handles scrolling itemCount: _sortedComments.length, itemBuilder: (context, index) { final comment = _sortedComments[index]; // --- Refactoring Note --- // Extracted the comment item into its own widget for clarity. return _CommentCard( comment: comment, timeAgo: _timeAgo(comment['date'] ?? ''), onPreviewImage: (imageUrls, idx) => _showImageViewer(imageUrls, idx), ); }, ), ], ); } // --- Helper and Builder methods --- Widget _buildDetailRow(String label, String? value, {required IconData icon}) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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 != null && value.isNotEmpty ? value : "-", color: Colors.black87, ), ), ], ), ); } Widget _buildSectionHeader(String title, IconData icon) { return Row( children: [ Icon(icon, size: 18, color: Colors.grey[700]), MySpacing.width(8), MyText.titleSmall(title, fontWeight: 600), ], ); } Widget _buildTeamMembers() { final teamMembersText = controller.basicValidator .getController(_FormFieldKeys.teamMembers) ?.text ?? ''; final members = teamMembersText .split(',') .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); if (members.isEmpty) return const SizedBox.shrink(); const double avatarSize = 32.0; const double avatarOverlap = 22.0; return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Row( children: [ Icon(Icons.group_outlined, size: 18, color: Colors.grey[700]), MySpacing.width(8), MyText.titleSmall("Team:", fontWeight: 600), MySpacing.width(12), GestureDetector( onTap: () => TeamBottomSheet.show( context: context, teamMembers: members.map((name) => _Member(name)).toList()), child: SizedBox( height: avatarSize, // Calculate width based on number of avatars shown width: (math.min(members.length, 3) * avatarOverlap) + (avatarSize - avatarOverlap), child: Stack( children: [ ...List.generate(math.min(members.length, 3), (i) { return Positioned( left: i * avatarOverlap, child: Tooltip( message: members[i], child: Avatar( firstName: members[i], lastName: '', size: avatarSize), ), ); }), if (members.length > 3) Positioned( left: 3 * avatarOverlap, child: CircleAvatar( radius: avatarSize / 2, backgroundColor: Colors.grey.shade300, child: MyText.bodySmall('+${members.length - 3}', fontWeight: 600), ), ), ], ), ), ), ], ), ); } Widget _buildPickerButton( {required VoidCallback onTap, required IconData icon, required String label}) { return Expanded( child: MyButton.outlined( onPressed: onTap, padding: MySpacing.xy(12, 10), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 18, color: Colors.blueAccent), MySpacing.width(8), MyText.bodySmall(label, color: Colors.blueAccent, fontWeight: 600), ], ), ), ); } // --- Action Handlers --- void _showCreateTaskBottomSheet() { 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(), ); } void _showImageViewer(List sources, int initialIndex) { showDialog( context: context, barrierColor: Colors.black87, builder: (_) => ImageViewerDialog( imageSources: sources, initialIndex: initialIndex, ), ); } Future _submitComment() async { if (controller.basicValidator.validateForm()) { await controller.commentTask( projectId: controller.basicValidator .getController(_FormFieldKeys.taskId) ?.text ?? '', comment: controller.basicValidator .getController(_FormFieldKeys.comment) ?.text ?? '', images: controller.selectedImages, ); // Callback to the parent widget to refresh data if needed widget.onCommentSuccess?.call(); } } } // --- Refactoring Note --- // A reusable widget for displaying a horizontal list of images. // It can handle both network URLs (String) and local files (File). class _ImageHorizontalListView extends StatelessWidget { final List imageSources; // Can be List or List final Function(int) onPreview; final Function(int)? onRemove; final Widget? emptyStatePlaceholder; const _ImageHorizontalListView({ required this.imageSources, required this.onPreview, this.onRemove, this.emptyStatePlaceholder, }); @override Widget build(BuildContext context) { if (imageSources.isEmpty) { return emptyStatePlaceholder ?? const SizedBox.shrink(); } return SizedBox( height: 70, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: imageSources.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final source = imageSources[index]; return GestureDetector( onTap: () => onPreview(index), child: Stack( clipBehavior: Clip.none, children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: source is File ? Image.file(source, width: 70, height: 70, fit: BoxFit.cover) : Image.network( source as String, 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]), ), ), ), if (onRemove != null) Positioned( top: -6, right: -6, child: GestureDetector( onTap: () => onRemove!(index), child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle), child: const Icon(Icons.close, size: 16, color: Colors.white), ), ), ), ], ), ); }, ), ); } } // --- Refactoring Note --- // A dedicated widget for a single comment card. This cleans up the main // widget's build method and makes the comment layout easier to manage. class _CommentCard extends StatelessWidget { final Map comment; final String timeAgo; final Function(List imageUrls, int index) onPreviewImage; const _CommentCard({ required this.comment, required this.timeAgo, required this.onPreviewImage, }); @override Widget build(BuildContext context) { final commentedBy = comment['commentedBy'] ?? 'Unknown'; final commentText = comment['text'] ?? '-'; final imageUrls = List.from(comment['preSignedUrls'] ?? []); return Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Avatar( firstName: commentedBy.split(' ').first, lastName: commentedBy.split(' ').length > 1 ? commentedBy.split(' ').last : '', size: 32, ), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium(commentedBy, fontWeight: 700, color: Colors.black87), MyText.bodySmall(timeAgo, color: Colors.black54, fontSize: 12), ], ), ), ], ), MySpacing.height(12), MyText.bodyMedium(commentText, color: Colors.black87), if (imageUrls.isNotEmpty) ...[ MySpacing.height(12), _ImageHorizontalListView( imageSources: imageUrls, onPreview: (index) => onPreviewImage(imageUrls, index), ), ], ], ), ); } }